From 2c53796ada5abacb6ded0c831fa5209fe6e03fee Mon Sep 17 00:00:00 2001 From: Dakota Brink Date: Mon, 30 Sep 2024 15:24:55 -0400 Subject: [PATCH] patchwork --- Cargo.lock | 2 + bindings_ffi/Cargo.lock | 2 + bindings_node/Cargo.lock | 2 + mls_validation_service/Cargo.toml | 6 +- mls_validation_service/src/handlers.rs | 217 ++++++++++++- xmtp_mls/Cargo.toml | 2 + .../grant_messaging_access_association.rs | 290 ++++++++++++++++++ .../legacy_create_identity_association.rs | 211 +++++++++++++ xmtp_mls/src/credential/mod.rs | 185 +++++++++++ xmtp_mls/src/lib.rs | 2 + xmtp_mls/src/verified_key_package.rs | 116 +++++++ .../src/gen/xmtp.identity.associations.rs | 102 +++--- 12 files changed, 1071 insertions(+), 66 deletions(-) create mode 100644 xmtp_mls/src/credential/grant_messaging_access_association.rs create mode 100644 xmtp_mls/src/credential/legacy_create_identity_association.rs create mode 100644 xmtp_mls/src/credential/mod.rs create mode 100644 xmtp_mls/src/verified_key_package.rs diff --git a/Cargo.lock b/Cargo.lock index 05bc6013d..4c7cb9029 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6300,6 +6300,7 @@ dependencies = [ "async-barrier", "async-stream", "bincode", + "chrono", "criterion", "ctor", "diesel", @@ -6342,6 +6343,7 @@ dependencies = [ "xmtp_cryptography", "xmtp_id", "xmtp_proto", + "xmtp_v2", ] [[package]] diff --git a/bindings_ffi/Cargo.lock b/bindings_ffi/Cargo.lock index f2e3192f8..b67e1446b 100644 --- a/bindings_ffi/Cargo.lock +++ b/bindings_ffi/Cargo.lock @@ -5759,6 +5759,7 @@ dependencies = [ "aes-gcm", "async-stream", "bincode", + "chrono", "diesel", "diesel_migrations", "ed25519-dalek", @@ -5788,6 +5789,7 @@ dependencies = [ "xmtp_cryptography", "xmtp_id", "xmtp_proto", + "xmtp_v2", ] [[package]] diff --git a/bindings_node/Cargo.lock b/bindings_node/Cargo.lock index 012d81248..c3b144198 100644 --- a/bindings_node/Cargo.lock +++ b/bindings_node/Cargo.lock @@ -5264,6 +5264,7 @@ dependencies = [ "aes-gcm", "async-stream", "bincode", + "chrono", "diesel", "diesel_migrations", "ed25519-dalek", @@ -5293,6 +5294,7 @@ dependencies = [ "xmtp_cryptography", "xmtp_id", "xmtp_proto", + "xmtp_v2", ] [[package]] diff --git a/mls_validation_service/Cargo.toml b/mls_validation_service/Cargo.toml index 6da9cbdb5..978ca83dc 100644 --- a/mls_validation_service/Cargo.toml +++ b/mls_validation_service/Cargo.toml @@ -11,6 +11,7 @@ path = "src/main.rs" clap = { version = "4.4.6", features = ["derive"] } ed25519-dalek = { workspace = true, features = ["digest"] } env_logger = "0.11" +ethers = { workspace = true } futures = { workspace = true } hex = { workspace = true } log = { workspace = true } @@ -25,10 +26,7 @@ tonic = { workspace = true } warp = "0.3.6" xmtp_id.workspace = true xmtp_mls.workspace = true -xmtp_proto = { path = "../xmtp_proto", features = [ - "proto_full", - "convert", -] } +xmtp_proto = { path = "../xmtp_proto", features = ["proto_full", "convert"] } [dev-dependencies] anyhow.workspace = true diff --git a/mls_validation_service/src/handlers.rs b/mls_validation_service/src/handlers.rs index 67a93e9e2..27ffb6f2e 100644 --- a/mls_validation_service/src/handlers.rs +++ b/mls_validation_service/src/handlers.rs @@ -1,18 +1,19 @@ use ethers::types::{BlockNumber, Bytes, U64}; use futures::future::{join_all, try_join_all}; -use openmls::prelude::{tls_codec::Deserialize, MlsMessageIn, ProtocolMessage}; +use openmls::prelude::{tls_codec::Deserialize, BasicCredential, MlsMessageIn, ProtocolMessage}; use openmls_rust_crypto::RustCrypto; use tonic::{Request, Response, Status}; use xmtp_id::{ associations::{ self, try_map_vec, unverified::UnverifiedIdentityUpdate, AccountId, AssociationError, - DeserializationError, SignatureError, + DeserializationError, MemberIdentifier, SignatureError, }, scw_verifier::SmartContractSignatureVerifier, }; use xmtp_mls::{ utils::id::serialize_group_id, + verified_key_package::VerifiedKeyPackage, verified_key_package_v2::{KeyPackageVerificationError, VerifiedKeyPackageV2}, }; use xmtp_proto::xmtp::{ @@ -22,12 +23,16 @@ use xmtp_proto::xmtp::{ mls_validation::v1::{ validate_group_messages_response::ValidationResponse as ValidateGroupMessageValidationResponse, validate_inbox_id_key_packages_response::Response as ValidateInboxIdKeyPackageResponse, + validate_inbox_ids_request::ValidationRequest as InboxIdValidationRequest, + validate_inbox_ids_response::ValidationResponse as InboxIdValidationResponse, validate_key_packages_response::ValidationResponse as ValidateKeyPackagesValidationResponse, + validate_key_packages_response::ValidationResponse as ValidateKeyPackageValidationResponse, validation_api_server::ValidationApi, GetAssociationStateRequest, GetAssociationStateResponse, ValidateGroupMessagesRequest, ValidateGroupMessagesResponse, ValidateInboxIdKeyPackagesRequest, ValidateInboxIdKeyPackagesResponse, - ValidateKeyPackagesRequest, ValidateKeyPackagesResponse, - VerifySmartContractWalletSignaturesRequest, VerifySmartContractWalletSignaturesResponse, + ValidateInboxIdsRequest, ValidateInboxIdsResponse, ValidateKeyPackagesRequest, + ValidateKeyPackagesResponse, VerifySmartContractWalletSignaturesRequest, + VerifySmartContractWalletSignaturesResponse, }, }; @@ -61,6 +66,57 @@ impl ValidationService { #[tonic::async_trait] impl ValidationApi for ValidationService { + async fn validate_inbox_ids( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let ValidateInboxIdsRequest { requests } = request.into_inner(); + let responses: Vec<_> = requests.into_iter().map(validate_inbox_id).collect(); + + let responses: Vec = join_all(responses) + .await + .into_iter() + .map(|res| res.map_err(InboxIdValidationResponse::from)) + .map(|r| r.unwrap_or_else(|e| e)) + .collect(); + Ok(Response::new(ValidateInboxIdsResponse { responses })) + } + + async fn validate_key_packages( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + let out: Vec = request + .into_inner() + .key_packages + .into_iter() + .map( + |kp| match validate_key_package(kp.key_package_bytes_tls_serialized) { + Ok(res) => ValidateKeyPackageValidationResponse { + is_ok: true, + error_message: "".to_string(), + installation_id: res.installation_id, + account_address: res.account_address, + credential_identity_bytes: res.credential_identity_bytes, + expiration: res.expiration, + }, + Err(e) => ValidateKeyPackageValidationResponse { + is_ok: false, + error_message: e, + installation_id: vec![], + account_address: "".to_string(), + credential_identity_bytes: vec![], + expiration: 0, + }, + }, + ) + .collect(); + + Ok(Response::new(ValidateKeyPackagesResponse { + responses: out, + })) + } + async fn validate_group_messages( &self, request: Request, @@ -116,8 +172,9 @@ impl ValidationApi for ValidationService { async fn validate_inbox_id_key_packages( &self, - request: Request, - ) -> Result, Status> { + request: tonic::Request, + ) -> std::result::Result, tonic::Status> + { let ValidateKeyPackagesRequest { key_packages } = request.into_inner(); let responses: Vec<_> = key_packages @@ -126,15 +183,125 @@ impl ValidationApi for ValidationService { .map(validate_inbox_id_key_package) .collect(); - let responses: Vec = join_all(responses) + let responses: Vec = join_all(responses) .await .into_iter() - .map(|res| res.map_err(ValidateInboxIdKeyPackageError::from)) + .map(|res| res.map_err(ValidateInboxIdKeyPackageResponse::from)) .map(|r| r.unwrap_or_else(|e| e)) .collect(); - Ok(Response::new(ValidateKeyPackagesResponse { responses })) + Ok(Response::new(ValidateInboxIdKeyPackagesResponse { + responses, + })) + } +} + +/// Error type for inbox ID validation +/// Each variant requires carrying the ID that failed to validate +/// The error variant itself becomes the failed version of `InboxIdValidationResponse` but allows +/// us to write normal rust in `validate_inbox_id` +#[derive(thiserror::Error, Debug)] +enum InboxIdValidationError { + #[error("Inbox ID {id} failed to validate")] + Deserialization { + id: String, + source: DeserializationError, + }, + #[error("Valid association state could not be found for inbox {id}, {source}")] + Association { + id: String, + source: AssociationError, + }, + #[error("Missing Credential")] + MissingCredential, + #[error("Inbox {id} is not associated with member {member}")] + MemberNotAssociated { + id: String, + member: MemberIdentifier, + }, + #[error( + "Given Inbox Id, {credential_inbox_id} does not match resulting inbox id, {state_inbox_id}" + )] + InboxIdDoesNotMatch { + credential_inbox_id: String, + state_inbox_id: String, + }, +} + +impl InboxIdValidationError { + pub fn inbox_id(&self) -> String { + match self { + InboxIdValidationError::Deserialization { id, .. } => id.clone(), + InboxIdValidationError::MissingCredential => "null".to_string(), + InboxIdValidationError::Association { id, .. } => id.clone(), + InboxIdValidationError::MemberNotAssociated { id, .. } => id.clone(), + InboxIdValidationError::InboxIdDoesNotMatch { + credential_inbox_id, + .. + } => credential_inbox_id.clone(), + } + } +} + +impl From for InboxIdValidationResponse { + fn from(err: InboxIdValidationError) -> Self { + InboxIdValidationResponse { + is_ok: false, + error_message: err.to_string(), + inbox_id: err.inbox_id(), + } + } +} + +async fn validate_inbox_id( + request: InboxIdValidationRequest, +) -> Result { + let InboxIdValidationRequest { + credential, + installation_public_key, + identity_updates, + } = request; + + if credential.is_none() { + return Err(InboxIdValidationError::MissingCredential); + } + + let inbox_id = credential.expect("checked for empty credential").inbox_id; + + let state = associations::get_state(try_map_vec(identity_updates).map_err(|e| { + InboxIdValidationError::Deserialization { + source: e, + id: inbox_id.clone(), + } + })?) + .await + .map_err(|e| InboxIdValidationError::Association { + source: e, + id: inbox_id.clone(), + })?; + + // this is defensive and should not happen. + // The only way an inbox id is different is if xmtp-node-go hands over identity updates with a different inbox id. + // which is a bug. + if state.inbox_id().as_ref() != *inbox_id { + return Err(InboxIdValidationError::InboxIdDoesNotMatch { + credential_inbox_id: inbox_id.clone(), + state_inbox_id: state.inbox_id().clone(), + }); + } + + let member = MemberIdentifier::Installation(installation_public_key); + if state.get(&member).is_none() { + return Err(InboxIdValidationError::MemberNotAssociated { + id: inbox_id, + member, + }); } + Ok(InboxIdValidationResponse { + is_ok: true, + error_message: "".to_string(), + inbox_id, + }) } #[derive(thiserror::Error, Debug)] @@ -174,7 +341,7 @@ async fn validate_inbox_id_key_package( async fn verify_smart_contract_wallet_signatures( signatures: Vec, scw_verifier: &dyn SmartContractSignatureVerifier, -) -> Result { +) -> Result, Status> { let mut futures = vec![]; for sig in signatures { @@ -186,7 +353,9 @@ async fn verify_smart_contract_wallet_signatures( )); } - Ok(VerifySmartContractWalletSignaturesResponse { responses: vec![] }) + Ok(Response::new(VerifySmartContractWalletSignaturesResponse { + responses: vec![], + })) } async fn get_association_state( @@ -248,6 +417,32 @@ fn validate_group_message(message: Vec) -> Result, + account_address: String, + credential_identity_bytes: Vec, + expiration: u64, +} + +fn validate_key_package(key_package_bytes: Vec) -> Result { + let rust_crypto = RustCrypto::default(); + let verified_key_package = + VerifiedKeyPackage::from_bytes(&rust_crypto, key_package_bytes.as_slice()) + .map_err(|e| e.to_string())?; + + let credential = verified_key_package.inner.leaf_node().credential(); + + let basic_credential = + BasicCredential::try_from(credential.clone()).map_err(|e| e.to_string())?; + + Ok(ValidateKeyPackageResult { + installation_id: verified_key_package.installation_id(), + account_address: verified_key_package.account_address, + credential_identity_bytes: basic_credential.identity().to_vec(), + expiration: 0, + }) +} + #[cfg(test)] mod tests { use ed25519_dalek::SigningKey; diff --git a/xmtp_mls/Cargo.toml b/xmtp_mls/Cargo.toml index 6d3705a78..c94d47db5 100644 --- a/xmtp_mls/Cargo.toml +++ b/xmtp_mls/Cargo.toml @@ -28,6 +28,7 @@ test-utils = ["xmtp_id/test-utils"] aes-gcm = { version = "0.10.3", features = ["std"] } async-stream.workspace = true bincode = "1.3.3" +chrono.workspace = true diesel = { version = "2.2.2", features = [ "sqlite", "r2d2", @@ -64,6 +65,7 @@ trait-variant.workspace = true xmtp_cryptography = { workspace = true } xmtp_id = { path = "../xmtp_id" } xmtp_proto = { workspace = true, features = ["proto_full", "convert"] } +xmtp_v2 = { path = "../xmtp_v2" } # Test/Bench Utils anyhow = { workspace = true, optional = true } diff --git a/xmtp_mls/src/credential/grant_messaging_access_association.rs b/xmtp_mls/src/credential/grant_messaging_access_association.rs new file mode 100644 index 000000000..86a6cce00 --- /dev/null +++ b/xmtp_mls/src/credential/grant_messaging_access_association.rs @@ -0,0 +1,290 @@ +use chrono::DateTime; +use serde::{Deserialize, Serialize}; + +use xmtp_cryptography::signature::{ + ed25519_public_key_to_address, sanitize_evm_addresses, RecoverableSignature, +}; +use xmtp_proto::xmtp::mls::message_contents::{ + GrantMessagingAccessAssociation as GrantMessagingAccessAssociationProto, + RecoverableEcdsaSignature as RecoverableEcdsaSignatureProto, +}; + +use crate::{types::Address, utils::time::NS_IN_SEC, InboxOwner}; + +use super::AssociationError; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct UnsignedGrantMessagingAccessData { + pub(crate) account_address: Address, + pub(crate) installation_public_key: Vec, + pub(crate) created_ns: u64, + iso8601_time: String, +} + +impl UnsignedGrantMessagingAccessData { + pub fn new( + account_address: Address, + installation_public_key: Vec, + created_ns: u64, + ) -> Result { + let account_address = sanitize_evm_addresses(vec![account_address])?[0].clone(); + let created_time = DateTime::from_timestamp( + created_ns as i64 / NS_IN_SEC, + (created_ns as i64 % NS_IN_SEC) as u32, + ) + .ok_or(AssociationError::MalformedAssociation)?; + let iso8601_time = format!("{}", created_time.format("%+")); + + Ok(Self { + account_address, + installation_public_key, + created_ns, + iso8601_time, + }) + } + + pub fn account_address(&self) -> Address { + self.account_address.clone() + } + + pub fn installation_public_key(&self) -> Vec { + self.installation_public_key.clone() + } + + pub fn created_ns(&self) -> u64 { + self.created_ns + } + + fn header_text() -> String { + let label = "Grant Messaging Access".to_string(); + format!("XMTP : {}", label) + } + + fn body_text( + account_address: &Address, + installation_public_key: &[u8], + iso8601_time: &str, + ) -> String { + format!( + "\nCurrent Time: {}\nAccount Address: {}\nInstallation ID: {}", + iso8601_time, + account_address, + ed25519_public_key_to_address(installation_public_key) + ) + } + + fn footer_text() -> String { + "For more info: https://xmtp.org/signatures/".to_string() + } + + pub fn text(&self) -> String { + format!( + "{}\n{}\n\n{}", + Self::header_text(), + Self::body_text( + &self.account_address, + &self.installation_public_key, + &self.iso8601_time + ), + Self::footer_text() + ) + .to_string() + } +} + +/// An Association is link between a blockchain account and an xmtp installation for the purposes of +/// authentication. +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct GrantMessagingAccessAssociation { + association_data: UnsignedGrantMessagingAccessData, + signature: RecoverableSignature, +} + +impl GrantMessagingAccessAssociation { + pub(crate) fn new_validated( + association_data: UnsignedGrantMessagingAccessData, + signature: RecoverableSignature, + ) -> Result { + let this = Self { + association_data, + signature, + }; + this.is_valid()?; + Ok(this) + } + + pub(crate) fn create( + owner: &impl InboxOwner, + installation_public_key: Vec, + created_ns: u64, + ) -> Result { + let unsigned_data = UnsignedGrantMessagingAccessData::new( + owner.get_address(), + installation_public_key, + created_ns, + )?; + let text = unsigned_data.text(); + let signature = owner.sign(&text)?; + Self::new_validated(unsigned_data, signature) + } + + pub fn from_proto_validated( + proto: GrantMessagingAccessAssociationProto, + expected_installation_public_key: &[u8], + ) -> Result { + let signature = RecoverableSignature::Eip191Signature( + proto + .signature + .ok_or(AssociationError::MalformedAssociation)? + .bytes, + ); + Self::new_validated( + UnsignedGrantMessagingAccessData::new( + proto.account_address, + expected_installation_public_key.to_vec(), + proto.created_ns, + )?, + signature, + ) + } + + fn is_valid(&self) -> Result<(), AssociationError> { + let assumed_addr = self.association_data.account_address(); + + let addr = self + .signature + .recover_address(&self.association_data.text())?; + + let sanitized_addresses = sanitize_evm_addresses(vec![assumed_addr, addr])?; + if sanitized_addresses[0] != sanitized_addresses[1] { + Err(AssociationError::AddressMismatch { + provided_addr: sanitized_addresses[0].clone(), + signing_addr: sanitized_addresses[1].clone(), + }) + } else { + Ok(()) + } + } + + pub fn account_address(&self) -> String { + self.association_data.account_address() + } + + pub fn installation_public_key(&self) -> Vec { + self.association_data.installation_public_key() + } + + pub fn created_ns(&self) -> u64 { + self.association_data.created_ns() + } +} + +impl From for GrantMessagingAccessAssociationProto { + fn from(assoc: GrantMessagingAccessAssociation) -> Self { + let account_address = assoc.account_address(); + let created_ns = assoc.created_ns(); + Self { + account_address, + // Hardcoded version for now + association_text_version: 1, + signature: Some(RecoverableEcdsaSignatureProto { + bytes: assoc.signature.into(), + }), + created_ns, + } + } +} + +#[cfg(test)] +pub mod tests { + use ethers::signers::{LocalWallet, Signer}; + + use xmtp_cryptography::{signature::h160addr_to_string, utils::rng}; + use xmtp_proto::xmtp::mls::message_contents::GrantMessagingAccessAssociation as GrantMessagingAccessAssociationProto; + + use crate::credential::{ + grant_messaging_access_association::GrantMessagingAccessAssociation, + UnsignedGrantMessagingAccessData, + }; + + #[tokio::test] + async fn assoc_gen() { + let key_bytes = vec![22, 33, 44, 55]; + + let wallet = LocalWallet::new(&mut rng()); + let other_wallet = LocalWallet::new(&mut rng()); + let addr = h160addr_to_string(wallet.address()); + let other_addr = h160addr_to_string(other_wallet.address()); + let grant_time = 1609459200000000; + let bad_grant_time = 1609459200000001; + + let data = + UnsignedGrantMessagingAccessData::new(addr.clone(), key_bytes.clone(), grant_time) + .unwrap(); + let sig = wallet.sign_message(data.text()).await.expect("BadSign"); + + let other_data = UnsignedGrantMessagingAccessData::new( + other_addr.clone(), + key_bytes.clone(), + grant_time, + ) + .unwrap(); + let other_sig = wallet + .sign_message(other_data.text()) + .await + .expect("BadSign"); + + let bad_key_bytes = vec![11, 22, 33]; + + assert!(GrantMessagingAccessAssociation::new_validated( + UnsignedGrantMessagingAccessData::new(addr.clone(), key_bytes.clone(), grant_time) + .unwrap(), + sig.into() + ) + .is_ok()); + assert!(GrantMessagingAccessAssociation::new_validated( + UnsignedGrantMessagingAccessData::new(addr.clone(), bad_key_bytes.clone(), grant_time) + .unwrap(), + sig.into() + ) + .is_err()); + assert!(GrantMessagingAccessAssociation::new_validated( + UnsignedGrantMessagingAccessData::new( + other_addr.clone(), + key_bytes.clone(), + grant_time, + ) + .unwrap(), + sig.into() + ) + .is_err()); + assert!(GrantMessagingAccessAssociation::new_validated( + UnsignedGrantMessagingAccessData::new(addr.clone(), key_bytes.clone(), bad_grant_time) + .unwrap(), + sig.into() + ) + .is_err()); + assert!(GrantMessagingAccessAssociation::new_validated( + UnsignedGrantMessagingAccessData::new(addr.clone(), key_bytes.clone(), grant_time) + .unwrap(), + other_sig.into() + ) + .is_err()); + } + + #[tokio::test] + async fn to_proto() { + let key_bytes = vec![22, 33, 44, 55]; + let wallet = LocalWallet::new(&mut rng()); + let addr = h160addr_to_string(wallet.address()); + let created_ns = 1609459200000000; + let data = UnsignedGrantMessagingAccessData::new(addr, key_bytes, created_ns).unwrap(); + let sig = wallet.sign_message(data.text()).await.expect("BadSign"); + + let assoc = GrantMessagingAccessAssociation::new_validated(data, sig.into()).unwrap(); + let proto_signature: GrantMessagingAccessAssociationProto = assoc.into(); + + assert_eq!(proto_signature.association_text_version, 1); + assert_eq!(proto_signature.signature.unwrap().bytes, sig.to_vec()); + } +} diff --git a/xmtp_mls/src/credential/legacy_create_identity_association.rs b/xmtp_mls/src/credential/legacy_create_identity_association.rs new file mode 100644 index 000000000..c974064e0 --- /dev/null +++ b/xmtp_mls/src/credential/legacy_create_identity_association.rs @@ -0,0 +1,211 @@ +use prost::Message; + +use xmtp_id::associations::signature::ValidatedLegacySignedPublicKey; +use xmtp_proto::xmtp::{ + message_contents::{signed_private_key, SignedPrivateKey as LegacySignedPrivateKeyProto}, + mls::message_contents::{ + LegacyCreateIdentityAssociation as LegacyCreateIdentityAssociationProto, + RecoverableEcdsaSignature as RecoverableEcdsaSignatureProto, + }, +}; +use xmtp_v2::k256_helper; + +use super::AssociationError; + +/// An Association is link between a blockchain account and an xmtp installation for the purposes of +/// authentication. +pub struct LegacyCreateIdentityAssociation { + installation_public_key: Vec, + delegating_signature: Vec, + legacy_signed_public_key: ValidatedLegacySignedPublicKey, +} + +impl LegacyCreateIdentityAssociation { + fn new_validated( + installation_public_key: Vec, + delegating_signature: Vec, + legacy_signed_public_key: ValidatedLegacySignedPublicKey, + ) -> Result { + let this = Self { + installation_public_key, + delegating_signature, + legacy_signed_public_key, + }; + this.is_valid()?; + Ok(this) + } + + pub(crate) fn create( + legacy_signed_private_key: Vec, + installation_public_key: Vec, + ) -> Result { + let legacy_signed_private_key_proto = + LegacySignedPrivateKeyProto::decode(legacy_signed_private_key.as_slice())?; + let signed_private_key::Union::Secp256k1(secp256k1) = legacy_signed_private_key_proto + .union + .ok_or(AssociationError::MalformedLegacyKey( + "Missing secp256k1.union field".to_string(), + ))?; + let legacy_private_key = secp256k1.bytes; + let (mut delegating_signature, recovery_id) = k256_helper::sign_sha256( + &legacy_private_key, // secret_key + &installation_public_key, // message + ) + .map_err(AssociationError::LegacySignature)?; + delegating_signature.push(recovery_id); // TODO: normalize recovery ID if necessary + + let legacy_signed_public_key_proto = legacy_signed_private_key_proto.public_key.ok_or( + AssociationError::MalformedLegacyKey("Missing public_key field".to_string()), + )?; + Self::new_validated( + installation_public_key, + delegating_signature, + legacy_signed_public_key_proto.try_into()?, // ValidatedLegacySignedPublicKey + ) + } + + pub fn from_proto_validated( + proto: LegacyCreateIdentityAssociationProto, + expected_installation_public_key: &[u8], + ) -> Result { + let delegating_signature = proto + .signature + .ok_or(AssociationError::MalformedAssociation)? + .bytes; + let legacy_signed_public_key_proto = proto + .signed_legacy_create_identity_key + .ok_or(AssociationError::MalformedAssociation)?; + + Self::new_validated( + expected_installation_public_key.to_vec(), + delegating_signature, + legacy_signed_public_key_proto.try_into()?, // ValidatedLegacySignedPublicKey + ) + } + + fn is_valid(&self) -> Result<(), AssociationError> { + // Validate legacy key signs installation key + if self.delegating_signature.len() != 65 { + return Err(AssociationError::MalformedAssociation); + } + assert!(k256_helper::verify_sha256( + &self.legacy_signed_public_key.key_bytes(), // signed_by + &self.installation_public_key, // message + &self.delegating_signature[0..64], // signature + self.delegating_signature[64], // recovery_id + ) + .map_err(AssociationError::LegacySignature)?); // always returns true if no error + + // Wallet signature of legacy key is internally validated by ValidatedLegacySignedPublicKey on creation + Ok(()) + } + + pub fn account_address(&self) -> String { + self.legacy_signed_public_key.account_address() + } + + pub fn installation_public_key(&self) -> Vec { + self.installation_public_key.clone() + } + + pub fn created_ns(&self) -> u64 { + self.legacy_signed_public_key.created_ns() + } +} + +impl From for LegacyCreateIdentityAssociationProto { + fn from(assoc: LegacyCreateIdentityAssociation) -> Self { + Self { + signature: Some(RecoverableEcdsaSignatureProto { + bytes: assoc.delegating_signature.clone(), + }), + signed_legacy_create_identity_key: Some(assoc.legacy_signed_public_key.into()), + } + } +} + +#[cfg(test)] +pub mod tests { + use openmls_basic_credential::SignatureKeyPair; + use xmtp_proto::xmtp::mls::message_contents::LegacyCreateIdentityAssociation as LegacyCreateIdentityAssociationProto; + + use crate::{ + assert_err, + configuration::CIPHERSUITE, + credential::{ + legacy_create_identity_association::LegacyCreateIdentityAssociation, AssociationError, + }, + }; + + #[tokio::test] + async fn validate_serialization_round_trip() { + let legacy_address = "0x419cb1fa5635b0c6df47c9dc5765c8f1f4dff78e"; + let legacy_signed_private_key_proto = vec![ + 8, 128, 154, 196, 133, 220, 244, 197, 216, 23, 18, 34, 10, 32, 214, 70, 104, 202, 68, + 204, 25, 202, 197, 141, 239, 159, 145, 249, 55, 242, 147, 126, 3, 124, 159, 207, 96, + 135, 134, 122, 60, 90, 82, 171, 131, 162, 26, 153, 1, 10, 79, 8, 128, 154, 196, 133, + 220, 244, 197, 216, 23, 26, 67, 10, 65, 4, 232, 32, 50, 73, 113, 99, 115, 168, 104, + 229, 206, 24, 217, 132, 223, 217, 91, 63, 137, 136, 50, 89, 82, 186, 179, 150, 7, 127, + 140, 10, 165, 117, 233, 117, 196, 134, 227, 143, 125, 210, 187, 77, 195, 169, 162, 116, + 34, 20, 196, 145, 40, 164, 246, 139, 197, 154, 233, 190, 148, 35, 131, 240, 106, 103, + 18, 70, 18, 68, 10, 64, 90, 24, 36, 99, 130, 246, 134, 57, 60, 34, 142, 165, 221, 123, + 63, 27, 138, 242, 195, 175, 212, 146, 181, 152, 89, 48, 8, 70, 104, 94, 163, 0, 25, + 196, 228, 190, 49, 108, 141, 60, 174, 150, 177, 115, 229, 138, 92, 105, 170, 226, 204, + 249, 206, 12, 37, 145, 3, 35, 226, 15, 49, 20, 102, 60, 16, 1, + ]; + let installation_keys = SignatureKeyPair::new(CIPHERSUITE.signature_algorithm()).unwrap(); + + let assoc = LegacyCreateIdentityAssociation::create( + legacy_signed_private_key_proto, + installation_keys.to_public_vec(), + ) + .unwrap(); + + let proto: LegacyCreateIdentityAssociationProto = assoc.into(); + let assoc = LegacyCreateIdentityAssociation::from_proto_validated( + proto, + &installation_keys.to_public_vec(), + ) + .unwrap(); + assert_eq!(assoc.account_address(), legacy_address); + assert_eq!( + assoc.installation_public_key(), + installation_keys.to_public_vec() + ); + } + + #[tokio::test] + async fn validate_bad_signature() { + // let legacy_address = "0x419Cb1fA5635b0c6Df47c9DC5765c8f1f4DfF78e"; + let legacy_signed_private_key_proto = vec![ + 8, 128, 154, 196, 133, 220, 244, 197, 216, 23, 18, 34, 10, 32, 214, 70, 104, 202, 68, + 204, 25, 202, 197, 141, 239, 159, 145, 249, 55, 242, 147, 126, 3, 124, 159, 207, 96, + 135, 134, 122, 60, 90, 82, 171, 131, 162, 26, 153, 1, 10, 79, 8, 128, 154, 196, 133, + 220, 244, 197, 216, 23, 26, 67, 10, 65, 4, 232, 32, 50, 73, 113, 99, 115, 168, 104, + 229, 206, 24, 217, 132, 223, 217, 91, 63, 137, 136, 50, 89, 82, 186, 179, 150, 7, 127, + 140, 10, 165, 117, 233, 117, 196, 134, 227, 143, 125, 210, 187, 77, 195, 169, 162, 116, + 34, 20, 196, 145, 40, 164, 246, 139, 197, 154, 233, 190, 148, 35, 131, 240, 106, 103, + 18, 70, 18, 68, 10, 64, 90, 24, 36, 99, 130, 246, 134, 57, 60, 34, 142, 165, 221, 123, + 63, 27, 138, 242, 195, 175, 212, 146, 181, 152, 89, 48, 8, 70, 104, 94, 163, 0, 25, + 196, 228, 190, 49, 108, 141, 60, 174, 150, 177, 115, 229, 138, 92, 105, 170, 226, 204, + 249, 206, 12, 37, 145, 3, 35, 226, 15, 49, 20, 102, 60, 16, 1, + ]; + let installation_keys = SignatureKeyPair::new(CIPHERSUITE.signature_algorithm()).unwrap(); + + let mut assoc = LegacyCreateIdentityAssociation::create( + legacy_signed_private_key_proto, + installation_keys.to_public_vec(), + ) + .unwrap(); + assoc.delegating_signature[0] ^= 1; + + let proto: LegacyCreateIdentityAssociationProto = assoc.into(); + assert_err!( + LegacyCreateIdentityAssociation::from_proto_validated( + proto, + &installation_keys.to_public_vec(), + ), + AssociationError::LegacySignature(_) + ); + } +} diff --git a/xmtp_mls/src/credential/mod.rs b/xmtp_mls/src/credential/mod.rs new file mode 100644 index 000000000..44e137ae3 --- /dev/null +++ b/xmtp_mls/src/credential/mod.rs @@ -0,0 +1,185 @@ +mod grant_messaging_access_association; +mod legacy_create_identity_association; + +use openmls_basic_credential::SignatureKeyPair; +use prost::{DecodeError, Message}; +use thiserror::Error; + +use xmtp_cryptography::signature::AddressValidationError; +use xmtp_cryptography::signature::{RecoverableSignature, SignatureError}; +use xmtp_proto::xmtp::mls::message_contents::{ + mls_credential::Association as AssociationProto, MlsCredential as MlsCredentialProto, +}; + +use crate::{types::Address, utils::time::now_ns, InboxOwner}; + +pub use self::grant_messaging_access_association::GrantMessagingAccessAssociation; +pub use self::grant_messaging_access_association::UnsignedGrantMessagingAccessData; +pub use self::legacy_create_identity_association::LegacyCreateIdentityAssociation; + +#[derive(Debug, Error)] +pub enum AssociationError { + #[error("bad signature")] + BadSignature(#[from] SignatureError), + #[error("decode error: {0}")] + DecodeError(#[from] DecodeError), + #[error("legacy key: {0}")] + MalformedLegacyKey(String), + #[error("legacy signature: {0}")] + LegacySignature(String), + #[error("Association text mismatch")] + TextMismatch, + #[error("Installation public key mismatch")] + InstallationPublicKeyMismatch, + #[error( + "Address mismatch in Association: Provided:{provided_addr:?} != signed:{signing_addr:?}" + )] + AddressMismatch { + provided_addr: Address, + signing_addr: Address, + }, + #[error(transparent)] + AddressValidationError(#[from] AddressValidationError), + #[error("Malformed association")] + MalformedAssociation, + + #[error(transparent)] + // TODO: remove this AssociationError and use [xmtp_id::associations::AssociationError] + IDAssociationError(#[from] xmtp_id::associations::AssociationError), + #[error(transparent)] + SignatureError(#[from] xmtp_id::associations::SignatureError), +} + +pub enum Credential { + GrantMessagingAccess(GrantMessagingAccessAssociation), + LegacyCreateIdentity(LegacyCreateIdentityAssociation), +} + +impl Credential { + pub fn create( + installation_keys: &SignatureKeyPair, + owner: &impl InboxOwner, + ) -> Result { + let created_ns = now_ns() as u64; + let association = GrantMessagingAccessAssociation::create( + owner, + installation_keys.to_public_vec(), + created_ns, + )?; + Ok(Self::GrantMessagingAccess(association)) + } + + pub fn create_from_external_signer( + association_data: UnsignedGrantMessagingAccessData, + signature: Vec, + ) -> Result { + let association = GrantMessagingAccessAssociation::new_validated( + association_data, + RecoverableSignature::Eip191Signature(signature), + )?; + Ok(Self::GrantMessagingAccess(association)) + } + + pub fn create_from_legacy( + installation_keys: &SignatureKeyPair, + legacy_signed_private_key: Vec, + ) -> Result { + let association = LegacyCreateIdentityAssociation::create( + legacy_signed_private_key, + installation_keys.to_public_vec(), + )?; + Ok(Self::LegacyCreateIdentity(association)) + } + + pub fn from_proto_validated( + proto: MlsCredentialProto, + expected_account_address: Option<&str>, // Must validate when fetching identity updates + expected_installation_public_key: Option<&[u8]>, // Must cross-reference against leaf node when relevant + ) -> Result { + let credential = match proto + .association + .ok_or(AssociationError::MalformedAssociation)? + { + AssociationProto::MessagingAccess(assoc) => { + GrantMessagingAccessAssociation::from_proto_validated( + assoc, + &proto.installation_public_key, + ) + .map(Credential::GrantMessagingAccess) + } + AssociationProto::LegacyCreateIdentity(assoc) => { + LegacyCreateIdentityAssociation::from_proto_validated( + assoc, + &proto.installation_public_key, + ) + .map(Credential::LegacyCreateIdentity) + } + }?; + + if let Some(address) = expected_account_address { + if credential.address() != address { + return Err(AssociationError::AddressMismatch { + provided_addr: address.to_string(), + signing_addr: credential.address(), + }); + } + } + if let Some(public_key) = expected_installation_public_key { + if credential.installation_public_key() != public_key { + return Err(AssociationError::InstallationPublicKeyMismatch); + } + } + Ok(credential) + } + + pub fn address(&self) -> String { + match &self { + Credential::GrantMessagingAccess(assoc) => assoc.account_address(), + Credential::LegacyCreateIdentity(assoc) => assoc.account_address(), + } + } + + pub fn installation_public_key(&self) -> Vec { + match &self { + Credential::GrantMessagingAccess(assoc) => assoc.installation_public_key(), + Credential::LegacyCreateIdentity(assoc) => assoc.installation_public_key(), + } + } + + pub fn created_ns(&self) -> u64 { + match &self { + Credential::GrantMessagingAccess(assoc) => assoc.created_ns(), + Credential::LegacyCreateIdentity(assoc) => assoc.created_ns(), + } + } +} + +impl From for MlsCredentialProto { + fn from(credential: Credential) -> Self { + Self { + installation_public_key: credential.installation_public_key(), + association: match credential { + Credential::GrantMessagingAccess(assoc) => { + Some(AssociationProto::MessagingAccess(assoc.into())) + } + Credential::LegacyCreateIdentity(assoc) => { + Some(AssociationProto::LegacyCreateIdentity(assoc.into())) + } + }, + } + } +} + +pub fn get_validated_account_address( + credential: &[u8], + installation_public_key: &[u8], +) -> Result { + let proto = MlsCredentialProto::decode(credential)?; + let credential = Credential::from_proto_validated( + proto, + None, // expected_account_address + Some(installation_public_key), + )?; + + Ok(credential.address()) +} diff --git a/xmtp_mls/src/lib.rs b/xmtp_mls/src/lib.rs index 83afcb6bb..685e58c43 100644 --- a/xmtp_mls/src/lib.rs +++ b/xmtp_mls/src/lib.rs @@ -6,6 +6,7 @@ pub mod builder; pub mod client; pub mod codecs; pub mod configuration; +pub mod credential; pub mod groups; mod hpke; pub mod identity; @@ -16,6 +17,7 @@ pub mod storage; pub mod subscriptions; pub mod types; pub mod utils; +pub mod verified_key_package; pub mod verified_key_package_v2; mod xmtp_openmls_provider; diff --git a/xmtp_mls/src/verified_key_package.rs b/xmtp_mls/src/verified_key_package.rs new file mode 100644 index 000000000..0621d1355 --- /dev/null +++ b/xmtp_mls/src/verified_key_package.rs @@ -0,0 +1,116 @@ +use openmls::{ + credentials::{errors::BasicCredentialError, BasicCredential}, + prelude::{ + tls_codec::{Deserialize, Error as TlsCodecError}, + KeyPackage, KeyPackageIn, KeyPackageVerifyError, + }, +}; + +use openmls_rust_crypto::RustCrypto; +use thiserror::Error; + +use crate::{ + configuration::MLS_PROTOCOL_VERSION, + credential::{get_validated_account_address, AssociationError}, + identity::IdentityError, + types::Address, +}; + +#[derive(Debug, Error)] +pub enum KeyPackageVerificationError { + #[error("TLS Codec error: {0}")] + TlsError(#[from] TlsCodecError), + #[error("mls validation: {0}")] + MlsValidation(#[from] KeyPackageVerifyError), + #[error("identity: {0}")] + Identity(#[from] IdentityError), + #[error("invalid application id")] + InvalidApplicationId, + #[error("application id ({0}) does not match the credential address ({1}).")] + ApplicationIdCredentialMismatch(String, String), + #[error("invalid credential")] + InvalidCredential, + #[error(transparent)] + Association(#[from] AssociationError), + #[error("generic: {0}")] + Generic(String), + #[error("wrong credential type")] + WrongCredentialType(#[from] BasicCredentialError), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct VerifiedKeyPackage { + pub inner: KeyPackage, + pub account_address: String, +} + +impl VerifiedKeyPackage { + pub fn new(inner: KeyPackage, account_address: String) -> Self { + Self { + inner, + account_address, + } + } + + /// Validates starting with a KeyPackage (which is already validated by OpenMLS) + pub fn from_key_package(kp: KeyPackage) -> Result { + let leaf_node = kp.leaf_node(); + let basic_credential = BasicCredential::try_from(leaf_node.credential().clone())?; + let pub_key_bytes = leaf_node.signature_key().as_slice(); + let account_address = + identity_to_account_address(basic_credential.identity(), pub_key_bytes)?; + let application_id = extract_application_id(&kp)?; + if !account_address.eq(&application_id) { + return Err( + KeyPackageVerificationError::ApplicationIdCredentialMismatch( + application_id, + account_address, + ), + ); + } + + Ok(Self::new(kp, account_address)) + } + + // Validates starting with a KeyPackageIn as bytes (which is not validated by OpenMLS) + pub fn from_bytes( + crypto_provider: &RustCrypto, + data: &[u8], + ) -> Result { + let kp_in: KeyPackageIn = KeyPackageIn::tls_deserialize_exact(data)?; + let kp = kp_in.validate(crypto_provider, MLS_PROTOCOL_VERSION)?; + + Self::from_key_package(kp) + } + + pub fn installation_id(&self) -> Vec { + self.inner.leaf_node().signature_key().as_slice().to_vec() + } + + pub fn hpke_init_key(&self) -> Vec { + self.inner.hpke_init_key().as_slice().to_vec() + } +} + +fn identity_to_account_address( + credential_bytes: &[u8], + installation_key_bytes: &[u8], +) -> Result { + Ok(get_validated_account_address( + credential_bytes, + installation_key_bytes, + )?) +} + +fn extract_application_id(kp: &KeyPackage) -> Result { + let application_id_bytes = kp + .leaf_node() + .extensions() + .application_id() + .ok_or_else(|| KeyPackageVerificationError::InvalidApplicationId)? + .as_slice() + .to_vec(); + + String::from_utf8(application_id_bytes) + .map_err(|_| KeyPackageVerificationError::InvalidApplicationId) +} diff --git a/xmtp_proto/src/gen/xmtp.identity.associations.rs b/xmtp_proto/src/gen/xmtp.identity.associations.rs index 471e8137d..c79e47434 100644 --- a/xmtp_proto/src/gen/xmtp.identity.associations.rs +++ b/xmtp_proto/src/gen/xmtp.identity.associations.rs @@ -5,7 +5,7 @@ #[derive(Clone, PartialEq, ::prost::Message)] pub struct RecoverableEcdsaSignature { /// 65-bytes \[ R || S || V \], with recovery id as the last byte - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub bytes: ::prost::alloc::vec::Vec, } /// EdDSA signature for 25519 @@ -13,10 +13,10 @@ pub struct RecoverableEcdsaSignature { #[derive(Clone, PartialEq, ::prost::Message)] pub struct RecoverableEd25519Signature { /// 64 bytes \[R(32 bytes) || S(32 bytes)\] - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub bytes: ::prost::alloc::vec::Vec, /// 32 bytes - #[prost(bytes="vec", tag="2")] + #[prost(bytes = "vec", tag = "2")] pub public_key: ::prost::alloc::vec::Vec, } /// Smart Contract Wallet signature @@ -25,19 +25,19 @@ pub struct RecoverableEd25519Signature { pub struct SmartContractWalletSignature { /// CAIP-10 string /// - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub account_id: ::prost::alloc::string::String, /// Specify the block number to verify the signature against - #[prost(uint64, tag="2")] + #[prost(uint64, tag = "2")] pub block_number: u64, /// The actual signature bytes - #[prost(bytes="vec", tag="3")] + #[prost(bytes = "vec", tag = "3")] pub signature: ::prost::alloc::vec::Vec, /// The base 10 id of the EVM chain - #[prost(uint64, tag="4")] + #[prost(uint64, tag = "4")] pub chain_id: u64, /// A 32 byte hash - #[prost(bytes="vec", tag="5")] + #[prost(bytes = "vec", tag = "5")] pub hash: ::prost::alloc::vec::Vec, } /// An existing address on xmtpv2 may have already signed a legacy identity key @@ -49,9 +49,9 @@ pub struct SmartContractWalletSignature { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct LegacyDelegatedSignature { - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub delegated_key: ::core::option::Option, - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag = "2")] pub signature: ::core::option::Option, } /// A wrapper for all possible signature types @@ -63,7 +63,7 @@ pub struct Signature { /// recoverable, or specified as a field. /// 2. The signer certifies that the signing payload is correct. The payload /// must be inferred from the context in which the signature is provided. - #[prost(oneof="signature::Signature", tags="1, 2, 3, 4")] + #[prost(oneof = "signature::Signature", tags = "1, 2, 3, 4")] pub signature: ::core::option::Option, } /// Nested message and enum types in `Signature`. @@ -74,15 +74,15 @@ pub mod signature { /// 2. The signer certifies that the signing payload is correct. The payload /// must be inferred from the context in which the signature is provided. #[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Oneof)] + #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Signature { - #[prost(message, tag="1")] + #[prost(message, tag = "1")] Erc191(super::RecoverableEcdsaSignature), - #[prost(message, tag="2")] + #[prost(message, tag = "2")] Erc6492(super::SmartContractWalletSignature), - #[prost(message, tag="3")] + #[prost(message, tag = "3")] InstallationKey(super::RecoverableEd25519Signature), - #[prost(message, tag="4")] + #[prost(message, tag = "4")] DelegatedErc191(super::LegacyDelegatedSignature), } } @@ -90,17 +90,17 @@ pub mod signature { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct MemberIdentifier { - #[prost(oneof="member_identifier::Kind", tags="1, 2")] + #[prost(oneof = "member_identifier::Kind", tags = "1, 2")] pub kind: ::core::option::Option, } /// Nested message and enum types in `MemberIdentifier`. pub mod member_identifier { #[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Oneof)] + #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Kind { - #[prost(string, tag="1")] + #[prost(string, tag = "1")] Address(::prost::alloc::string::String), - #[prost(bytes, tag="2")] + #[prost(bytes, tag = "2")] InstallationPublicKey(::prost::alloc::vec::Vec), } } @@ -108,11 +108,11 @@ pub mod member_identifier { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Member { - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub identifier: ::core::option::Option, - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag = "2")] pub added_by_entity: ::core::option::Option, - #[prost(uint64, optional, tag="3")] + #[prost(uint64, optional, tag = "3")] pub client_timestamp_ns: ::core::option::Option, } /// The first entry of any XID log. The XID must be deterministically derivable @@ -122,12 +122,12 @@ pub struct Member { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CreateInbox { - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub initial_address: ::prost::alloc::string::String, - #[prost(uint64, tag="2")] + #[prost(uint64, tag = "2")] pub nonce: u64, /// Must be an addressable member - #[prost(message, optional, tag="3")] + #[prost(message, optional, tag = "3")] pub initial_address_signature: ::core::option::Option, } /// Adds a new member for an XID - either an addressable member such as a @@ -137,20 +137,20 @@ pub struct CreateInbox { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AddAssociation { - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub new_member_identifier: ::core::option::Option, - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag = "2")] pub existing_member_signature: ::core::option::Option, - #[prost(message, optional, tag="3")] + #[prost(message, optional, tag = "3")] pub new_member_signature: ::core::option::Option, } /// Revokes a member from an XID. The recovery address must sign the revocation. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct RevokeAssociation { - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub member_to_revoke: ::core::option::Option, - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag = "2")] pub recovery_address_signature: ::core::option::Option, } /// Changes the recovery address for an XID. The recovery address is not required @@ -159,30 +159,30 @@ pub struct RevokeAssociation { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ChangeRecoveryAddress { - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub new_recovery_address: ::prost::alloc::string::String, - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag = "2")] pub existing_recovery_address_signature: ::core::option::Option, } /// A single identity operation #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct IdentityAction { - #[prost(oneof="identity_action::Kind", tags="1, 2, 3, 4")] + #[prost(oneof = "identity_action::Kind", tags = "1, 2, 3, 4")] pub kind: ::core::option::Option, } /// Nested message and enum types in `IdentityAction`. pub mod identity_action { #[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Oneof)] + #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Kind { - #[prost(message, tag="1")] + #[prost(message, tag = "1")] CreateInbox(super::CreateInbox), - #[prost(message, tag="2")] + #[prost(message, tag = "2")] Add(super::AddAssociation), - #[prost(message, tag="3")] + #[prost(message, tag = "3")] Revoke(super::RevokeAssociation), - #[prost(message, tag="4")] + #[prost(message, tag = "4")] ChangeRecoveryAddress(super::ChangeRecoveryAddress), } } @@ -196,42 +196,42 @@ pub mod identity_action { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct IdentityUpdate { - #[prost(message, repeated, tag="1")] + #[prost(message, repeated, tag = "1")] pub actions: ::prost::alloc::vec::Vec, - #[prost(uint64, tag="2")] + #[prost(uint64, tag = "2")] pub client_timestamp_ns: u64, - #[prost(string, tag="3")] + #[prost(string, tag = "3")] pub inbox_id: ::prost::alloc::string::String, } /// Map of members belonging to an inbox_id #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct MemberMap { - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub key: ::core::option::Option, - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag = "2")] pub value: ::core::option::Option, } /// A final association state resulting from multiple `IdentityUpdates` #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AssociationState { - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub inbox_id: ::prost::alloc::string::String, - #[prost(message, repeated, tag="2")] + #[prost(message, repeated, tag = "2")] pub members: ::prost::alloc::vec::Vec, - #[prost(string, tag="3")] + #[prost(string, tag = "3")] pub recovery_address: ::prost::alloc::string::String, - #[prost(bytes="vec", repeated, tag="4")] + #[prost(bytes = "vec", repeated, tag = "4")] pub seen_signatures: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, } /// / state diff between two final AssociationStates #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct AssociationStateDiff { - #[prost(message, repeated, tag="1")] + #[prost(message, repeated, tag = "1")] pub new_members: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="2")] + #[prost(message, repeated, tag = "2")] pub removed_members: ::prost::alloc::vec::Vec, } /// Encoded file descriptor set for the `xmtp.identity.associations` package @@ -858,4 +858,4 @@ pub const FILE_DESCRIPTOR_SET: &[u8] = &[ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, ]; include!("xmtp.identity.associations.serde.rs"); -// @@protoc_insertion_point(module) \ No newline at end of file +// @@protoc_insertion_point(module)