Skip to main content
XMTP uses a multi-layered validation system to link MLS installation keys with wallet addresses through the Inbox ID system defined in XIP-46.

Validation Levels

LibXMTP performs validation at two levels:
  1. MLS protocol validation - Implemented by OpenMLS
  2. Application-level validation - XMTP-specific credential and identity validation

MLS Credentials

The MLS credential used in key packages and leaf nodes contains a single field: inbox_id.

Credential Structure

pub struct MlsCredential {
    pub inbox_id: String,
}
Validation requirements:
  1. Resolve the state of the inbox_id (as described in XIP-46)
  2. Ensure the installation key that signed the credential is a current member of the inbox
XMTP does not implement credential rotation because credentials represent a long-lived connection between the MLS client and the inbox_id.

Identity Association Types

When linking installation keys to an inbox, XMTP supports three signature types:

1. EOA (Externally Owned Account) Wallet Signatures

Standard Ethereum wallet signatures using ECDSA. Validation process:
  1. Recover the signer address using ECDSA signature recovery
  2. Verify the recovered address matches the expected address
  3. Ensure the signature text matches exactly (prevents random address recovery)
// Signature validation for EOA
let recovered_address = recover_signer(&signature, &signature_text)?;
assert_eq!(recovered_address, expected_address);
Location in code: crates/xmtp_id/src/associations/ident/ethereum.rs

2. Smart Contract Wallet Signatures

Smart contract wallets require on-chain validation per ERC-1271. Key characteristics:
  • Signatures are not recoverable (unlike EOA)
  • Validation requires calling the smart contract’s isValidSignature() function
  • Smart contract code is mutable - a valid signature may become invalid later
Smart contract signatures are validated at a specific block number to ensure deterministic validation. If the contract changes later, the association remains valid until explicitly revoked. This is a deliberate design choice.
Validation with EIP-6492: LibXMTP uses EIP-6492 with a “universal validator” smart contract, enabling verification of signatures from:
  • Deployed smart contract wallets
  • Counterfactual smart contract wallets (not yet deployed)
Requirements: Client applications must provide RPC URLs for all XMTP-supported blockchains during client instantiation. Location in code: crates/xmtp_id/src/scw_verifier/

3. V2 Legacy Key Signatures

XMTP V2 used a different identity model. To enable migration, V2 keys can create associations under strict limitations.
Critical constraint: Legacy V2 keys may only be used to create one association globally.
Enforcement mechanisms:
  1. V2 keys can only be used on an Inbox ID with nonce = 0
  2. Replay protection prevents reuse of V2 keys on the same inbox
Rationale: V2 keys were shared between multiple apps and devices. If compromised, all associated installations would need revocation. Limiting V2 keys to one association prevents cascading security issues. Validation chain:
Wallet Signature → V2 Public Key → XMTP Identity Key

            Challenge Text (for association)
Challenge text format:
XMTP : Create Identity
$SERIALIZED_V2_PUBLIC_KEY

For more info: https://xmtp.org/signatures/
Validation process:
  1. Recover wallet address from the signature on the V2 public key
  2. Verify the signature on the association challenge recovers to the same V2 public key
  3. If both validate, treat the association as if signed by the wallet
Location in code: crates/xmtp_id/src/associations/signature.rs:194-267

Inbox ID Creation

Inbox IDs are deterministically derived from wallet addresses:
inbox_id = SHA256(CONCAT(wallet_address, nonce))

CreateInbox Action

When creating a new inbox, the signature must be from an address where:
SHA256(CONCAT(wallet_address, nonce)) == inbox_id
Special case: If nonce = 0 and V2 keys are available, CreateInbox can be signed with V2 keys instead of the wallet (one-time migration).

AddAssociation Action

When adding an installation to an existing inbox:
  1. Installation keys sign the association text
  2. An existing member of the inbox must also sign (wallet or installation key)
  3. The existing member must not be revoked
V2 keys can only sign AddAssociation if nonce = 0 and the V2 keys have not been used for any previous identity updates.

Signature Re-use Protection

The system prevents signature replay attacks:

Between Inboxes

The inbox_id is included in all signature challenge text, making signatures inbox-specific.

Within an Inbox

Raw signature bytes are stored in the AssociationState:
pub struct AssociationState {
    pub(crate) inbox_id: String,
    pub(crate) members: HashMap<MemberIdentifier, Member>,
    pub(crate) recovery_identifier: Identifier,
    pub(crate) seen_signatures: HashSet<Vec<u8>>, // Prevents reuse
}
Each new identity action is checked against seen_signatures to prevent replay. Location: crates/xmtp_id/src/associations/state.rs:62-63

Key Package Validation

When validating another user’s key package:

Standard MLS Validation

  • Signature verification
  • Message authenticity checks
  • Lifetime validation
  • Protocol version compatibility

XMTP-Specific Validation

  1. Extract the credential:
    let credential = MlsCredential::decode(basic_credential.identity())?;
    
  2. Download identity updates:
    let identity_updates = api_client.get_identity_updates(credential.inbox_id).await?;
    
  3. Resolve association state:
    let state = get_state(identity_updates)?;
    
  4. Verify installation key is associated:
    let installation_key = key_package.leaf_node().signature_key();
    assert!(state.installation_ids().contains(installation_key));
    
Location: crates/xmtp_mls/src/verified_key_package_v2.rs

Commit Validation

In addition to OpenMLS validation, libxmtp validates commits by:
  1. Permission checks: Ensure the commit is allowed per group policies
  2. Credential validation: Validate credentials and key packages of new members
  3. Membership consistency: Ensure MLS group member changes match the expected diff from the GroupMembership extension
Currently, there is no mechanism to detect, report, or recover from group splits due to invalid commits. This may need to be handled at the application level.

Signature Normalization

To prevent signature malleability, XMTP normalizes ECDSA signatures to use the lower-s value:
pub fn to_lower_s(sig_bytes: &[u8]) -> Result<Vec<u8>, SignatureError> {
    let sig = K256Signature::try_from(sig_data)?;
    match sig.normalize_s() {
        None => sig_data.to_vec(), // Already normalized
        Some(normalized) => normalized.to_bytes().to_vec(),
    }
}
Location: crates/xmtp_id/src/associations/signature.rs:270-295

Build docs developers (and LLMs) love