-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
- Loading branch information
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
use prost::Message; | ||
use xmtp_mls::{ | ||
credential::{GrantMessagingAccessAssociation, LegacyCreateIdentityAssociation}, | ||
Check warning on line 3 in xmtp_id/src/credential.rs GitHub Actions / workspaceunused imports: `GrantMessagingAccessAssociation`, `LegacyCreateIdentityAssociation`
|
||
types::Address, | ||
}; | ||
use xmtp_proto::xmtp::mls::message_contents::MlsCredential as CredentialProto; | ||
|
||
pub enum AssociationType { | ||
ExternallyOwned, | ||
SmartContract, | ||
Legacy, | ||
} | ||
|
||
#[derive(thiserror::Error, Debug)] | ||
pub enum VerificationError { | ||
#[error( | ||
"Address mismatch in Association: Provided:{provided_addr:?} != signed:{signing_addr:?}" | ||
)] | ||
AddressMismatch { | ||
provided_addr: Address, | ||
signing_addr: Address, | ||
}, | ||
#[error("Installation public key mismatch")] | ||
InstallationPublicKeyMismatch, | ||
} | ||
|
||
pub struct VerifiedCredential { | ||
pub account_address: String, | ||
pub account_type: AssociationType, | ||
} | ||
|
||
impl VerifiedCredential { | ||
pub fn account_address(&self) -> String { | ||
self.account_address.clone() | ||
} | ||
|
||
pub fn account_type(&self) -> AssociationType { | ||
self.account_type.clone() | ||
} | ||
} | ||
|
||
pub struct VerificationRequest { | ||
expected_account_address: String, | ||
installation_public_key: Vec<u8>, | ||
credential: Vec<u8>, | ||
} | ||
|
||
type VerificationResult = Result<VerifiedCredential, VerificationError>; | ||
|
||
pub trait Credential { | ||
fn address(&self) -> String; | ||
fn installation_public_key(&self) -> Vec<u8>; | ||
fn created_ns(&self) -> u64; | ||
} | ||
|
||
#[async_trait::async_trait] | ||
pub trait CredentialVerifier { | ||
async fn verify_credential(request: VerificationRequest) -> VerificationResult; | ||
async fn batch_verify_credentials( | ||
credentials_to_verify: Vec<VerificationRequest>, | ||
) -> Vec<VerificationResult> { | ||
let mut results = Vec::new(); | ||
for credential in credentials_to_verify { | ||
results.push(Self::verify_credential(credential)); | ||
} | ||
|
||
futures::future::join_all(results).await | ||
} | ||
} | ||
|
||
impl<'a> Credential for &'a GrantMessagingAccessAssociation { | ||
fn address(&self) -> String { | ||
self.account_address().clone() | ||
} | ||
|
||
fn installation_public_key(&self) -> Vec<u8> { | ||
Check failure on line 76 in xmtp_id/src/credential.rs GitHub Actions / workspacethe trait bound `std::vec::Vec<u8>: prost::bytes::Buf` is not satisfied
|
||
self.installation_public_key().clone() | ||
} | ||
Check failure on line 78 in xmtp_id/src/credential.rs GitHub Actions / workspacemismatched types
Check failure on line 78 in xmtp_id/src/credential.rs GitHub Actions / workspacemismatched types
|
||
|
||
fn created_ns(&self) -> u64 { | ||
Check failure on line 80 in xmtp_id/src/credential.rs GitHub Actions / workspacemismatched types
|
||
GrantMessagingAccessAssociation::created_ns(self) | ||
} | ||
} | ||
|
||
Check failure on line 84 in xmtp_id/src/credential.rs GitHub Actions / workspacemismatched types
|
||
fn validate_credential( | ||
credential: impl Credential, | ||
request: VerificationRequest, | ||
) -> Result<(), VerificationError> { | ||
if credential.address() != request.expected_account_address { | ||
return Err(VerificationError::AddressMismatch { | ||
provided_addr: request.expected_account_address.to_string(), | ||
signing_addr: credential.address(), | ||
}); | ||
} | ||
|
||
if credential.installation_public_key() != request.installation_public_key { | ||
return Err(VerificationError::InstallationPublicKeyMismatch); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
#[async_trait::async_trait] | ||
impl CredentialVerifier for GrantMessagingAccessAssociation { | ||
async fn verify_credential(request: VerificationRequest) -> VerificationResult { | ||
let proto = CredentialProto::decode(request.credential); | ||
let credential = GrantMessagingAccessAssociation::from_proto_validated( | ||
proto, | ||
Some(request.installation_public_key), | ||
); | ||
validate_credential(&credential, request)?; | ||
|
||
Ok(VerifiedCredential { | ||
account_address: credential.account_address(), | ||
account_type: AssociationType::EOA, | ||
}) | ||
} | ||
} | ||
|
||
#[async_trait::async_trait] | ||
impl CredentialVerifier for LegacyCreateIdentityAssociation { | ||
async fn verify_credential(request: VerificationRequest) -> VerificationResult { | ||
let proto = CredentialProto::decode(request.credential); | ||
let credential = LegacyCreateIdentityAssociation::from_proto_validated( | ||
proto, | ||
Some(request.installation_public_key), | ||
); | ||
validate_credential(&credential, request)?; | ||
|
||
Ok(VerifiedCredential { | ||
account_address: credential.account_address(), | ||
account_type: AssociationType::Legacy, | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
/// Key Package Verification (Copied from `xmtp_mls/src/verified_key_package.rs`) | ||
use openmls::prelude::{KeyPackage, KeyPackageIn, KeyPackageVerifyError}; | ||
use openmls_rust_crypto::RustCrypto; | ||
use thiserror::Error; | ||
use tls_codec::{Deserialize, Error as TlsSerializationError}; | ||
|
||
use crate::{error::IdentityError, Identity}; | ||
use xmtp_mls::{configuration::MLS_PROTOCOL_VERSION, types::Address}; | ||
|
||
#[derive(Debug, Error)] | ||
pub enum KeyPackageVerificationError { | ||
#[error("serialization error: {0}")] | ||
Serialization(#[from] TlsSerializationError), | ||
#[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 lifetime")] | ||
InvalidLifetime, | ||
#[error("generic: {0}")] | ||
Generic(String), | ||
} | ||
|
||
#[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<Self, KeyPackageVerificationError> { | ||
let leaf_node = kp.leaf_node(); | ||
let identity_bytes = leaf_node.credential().identity(); | ||
let pub_key_bytes = leaf_node.signature_key().as_slice(); | ||
let account_address = identity_to_account_address(identity_bytes, pub_key_bytes)?; | ||
let application_id = extract_application_id(&kp)?; | ||
if !account_address.eq(&application_id) { | ||
return Err( | ||
KeyPackageVerificationError::ApplicationIdCredentialMismatch( | ||
application_id, | ||
account_address, | ||
), | ||
); | ||
} | ||
if !kp.life_time().is_valid() { | ||
return Err(KeyPackageVerificationError::InvalidLifetime); | ||
} | ||
|
||
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<VerifiedKeyPackage, KeyPackageVerificationError> { | ||
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<u8> { | ||
self.inner.leaf_node().signature_key().as_slice().to_vec() | ||
} | ||
|
||
pub fn hpke_init_key(&self) -> Vec<u8> { | ||
self.inner.hpke_init_key().as_slice().to_vec() | ||
} | ||
} | ||
|
||
fn identity_to_account_address( | ||
credential_bytes: &[u8], | ||
installation_key_bytes: &[u8], | ||
) -> Result<String, KeyPackageVerificationError> { | ||
Ok(Identity::get_validated_account_address( | ||
credential_bytes, | ||
installation_key_bytes, | ||
)?) | ||
} | ||
|
||
fn extract_application_id(kp: &KeyPackage) -> Result<Address, KeyPackageVerificationError> { | ||
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) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use openmls::{ | ||
credentials::CredentialWithKey, | ||
extensions::{ | ||
ApplicationIdExtension, Extension, ExtensionType, Extensions, LastResortExtension, | ||
}, | ||
group::config::CryptoConfig, | ||
prelude::Capabilities, | ||
prelude_test::KeyPackage, | ||
versions::ProtocolVersion, | ||
}; | ||
use xmtp_cryptography::utils::generate_local_wallet; | ||
|
||
use xmtp_mls::{ | ||
builder::ClientBuilder, | ||
configuration::CIPHERSUITE, | ||
verified_key_package::{KeyPackageVerificationError, VerifiedKeyPackage}, | ||
}; | ||
|
||
#[tokio::test] | ||
async fn test_invalid_application_id() { | ||
let client = ClientBuilder::new_test_client(&generate_local_wallet()).await; | ||
let conn = client.store.conn().unwrap(); | ||
let provider = client.mls_provider(&conn); | ||
|
||
// Build a key package | ||
let last_resort = Extension::LastResort(LastResortExtension::default()); | ||
// Make sure the application id doesn't match the account address | ||
let invalid_application_id = "invalid application id".as_bytes(); | ||
let application_id = | ||
Extension::ApplicationId(ApplicationIdExtension::new(invalid_application_id)); | ||
let leaf_node_extensions = Extensions::single(application_id); | ||
let capabilities = Capabilities::new( | ||
None, | ||
Some(&[CIPHERSUITE]), | ||
Some(&[ExtensionType::LastResort, ExtensionType::ApplicationId]), | ||
None, | ||
None, | ||
); | ||
// TODO: Set expiration | ||
let kp = KeyPackage::builder() | ||
.leaf_node_capabilities(capabilities) | ||
.key_package_extensions(Extensions::single(last_resort)) | ||
.leaf_node_extensions(leaf_node_extensions) | ||
.build( | ||
CryptoConfig { | ||
ciphersuite: CIPHERSUITE, | ||
version: ProtocolVersion::default(), | ||
}, | ||
&provider, | ||
&client.identity.installation_keys, | ||
CredentialWithKey { | ||
credential: client.identity.credential().unwrap(), | ||
signature_key: client.identity.installation_keys.to_public_vec().into(), | ||
}, | ||
) | ||
.unwrap(); | ||
|
||
let verified_kp_result = VerifiedKeyPackage::from_key_package(kp); | ||
assert!(verified_kp_result.is_err()); | ||
assert_eq!( | ||
KeyPackageVerificationError::ApplicationIdCredentialMismatch( | ||
String::from_utf8(invalid_application_id.to_vec()).unwrap(), | ||
client.account_address() | ||
) | ||
.to_string(), | ||
verified_kp_result.err().unwrap().to_string() | ||
); | ||
} | ||
} |