Skip to main content

xmtp_mls/
identity.rs

1pub use xmtp_cryptography::GeneratePostQuantumKeyError;
2mod identity_ext;
3pub use identity_ext::*;
4
5use crate::XmtpApi;
6use crate::identity_updates::{get_association_state_with_verifier, load_identity_updates};
7use crate::worker::NeedsDbReconnect;
8use derive_builder::Builder;
9use openmls::prelude::HpkeKeyPair;
10use openmls::prelude::hash_ref::HashReference;
11use openmls::{
12    credentials::errors::BasicCredentialError,
13    extensions::Extension,
14    key_packages::KeyPackage,
15    prelude::{Credential as OpenMlsCredential, tls_codec::Serialize},
16};
17use openmls_traits::types::CryptoError;
18use std::sync::atomic::{AtomicBool, Ordering};
19use thiserror::Error;
20use tracing::debug;
21use tracing::info;
22use xmtp_api::ApiClientWrapper;
23use xmtp_common::ErrorCode;
24use xmtp_common::snippet::Snippet;
25use xmtp_common::time::now_ns;
26use xmtp_common::{RetryableError, retryable};
27use xmtp_configuration::KEY_PACKAGE_ROTATION_INTERVAL_NS;
28use xmtp_cryptography::signature::IdentifierValidationError;
29use xmtp_cryptography::{CredentialSign, XmtpInstallationCredential};
30use xmtp_db::TransactionOutcome::Continue;
31use xmtp_db::db_connection::DbConnection;
32use xmtp_db::identity::StoredIdentity;
33use xmtp_db::sql_key_store::{
34    KEY_PACKAGE_REFERENCES, KEY_PACKAGE_WRAPPER_PRIVATE_KEY, SqlKeyStoreError,
35};
36use xmtp_db::{ConnectionExt, MlsProviderExt, TransactionOutcome};
37use xmtp_db::{Fetch, StorageError};
38use xmtp_db::{XmtpOpenMlsProviderRef, prelude::*};
39use xmtp_id::associations::unverified::UnverifiedSignature;
40use xmtp_id::associations::{AssociationError, Identifier, InstallationKeyContext, PublicContext};
41use xmtp_id::key_package::KeyPackageVerificationError;
42use xmtp_id::scw_verifier::SmartContractSignatureVerifier;
43use xmtp_id::{
44    InboxId, InboxIdRef,
45    associations::{
46        MemberIdentifier,
47        builder::{SignatureRequest, SignatureRequestBuilder, SignatureRequestError},
48    },
49};
50use xmtp_proto::types::InstallationId;
51
52/**
53 * The identity strategy determines how the [`ClientBuilder`](crate::builder::ClientBuilder) constructs an identity on startup.
54 *
55 * [`IdentityStrategy::CreateIfNotFound`] will attempt to create a new identity if one isn't found in the store.
56 * This is the default behavior.
57 *
58 * [`IdentityStrategy::CreateForIdentifier`] acts as `CreateIfNotFound` for the inbox of an identifier,
59 * which the builder asks the backend for only after it has checked the backend's deployment.
60 *
61 * [`IdentityStrategy::CachedOnly`] will attempt to get an identity from the store. If not found, it will
62 * return an error. This is useful if you don't want to create a new identity on startup because the caller
63 * does not have access to a signer.
64 *
65 * `IdentityStrategy::ExternalIdentity` allows you to provide an already-constructed identity to the
66 * client. This is useful for testing and not expected to be used in production.
67 */
68#[derive(Debug, Clone)]
69pub enum IdentityStrategy {
70    /// Tries to get an identity from the disk store. If not found, getting one from backend.
71    CreateIfNotFound {
72        inbox_id: InboxId,
73        identifier: Identifier,
74        nonce: u64,
75    },
76    /// `CreateIfNotFound` for the inbox the backend associates with the
77    /// identifier, or else the inbox its registration at the nonce creates.
78    CreateForIdentifier { identifier: Identifier, nonce: u64 },
79    /// Identity that is already in the disk store
80    CachedOnly,
81    /// An already-built Identity for testing purposes
82    #[cfg(any(test, feature = "test-utils"))]
83    ExternalIdentity(Identity),
84}
85
86impl IdentityStrategy {
87    pub fn inbox_id(&self) -> Option<InboxIdRef<'_>> {
88        use IdentityStrategy::*;
89        match self {
90            CreateIfNotFound { inbox_id, .. } => Some(inbox_id),
91            _ => None,
92        }
93    }
94
95    /// Create a new Identity Strategy, with [`IdentityStrategy::CreateIfNotFound`].
96    /// If an Identity is not found in the local store, creates a new one.
97    #[tracing::instrument(level = "trace", skip_all)]
98    pub fn new(inbox_id: InboxId, identifier: Identifier, nonce: u64) -> Self {
99        Self::CreateIfNotFound {
100            inbox_id,
101            identifier,
102            nonce,
103        }
104    }
105
106    /// Create an [`IdentityStrategy::CreateForIdentifier`], for a caller that
107    /// does not know the identifier's inbox.
108    pub fn for_identifier(identifier: Identifier, nonce: u64) -> Self {
109        Self::CreateForIdentifier { identifier, nonce }
110    }
111
112    /// Replace [`IdentityStrategy::CreateForIdentifier`] with
113    /// [`IdentityStrategy::CreateIfNotFound`] for the identifier's inbox. Call
114    /// it only after the backend's deployment is checked: the request carries
115    /// the identifier.
116    pub(crate) async fn with_inbox<ApiClient: XmtpApi>(
117        self,
118        api_client: &ApiClientWrapper<ApiClient>,
119    ) -> Result<Self, IdentityError> {
120        let Self::CreateForIdentifier { identifier, nonce } = self else {
121            return Ok(self);
122        };
123        let found = api_client
124            .get_inbox_ids(vec![identifier.clone().into()])
125            .await?;
126        let inbox_id = match found.into_iter().next().flatten() {
127            Some(inbox_id) => inbox_id,
128            None => identifier.inbox_id(nonce)?,
129        };
130        Ok(Self::new(inbox_id, identifier, nonce))
131    }
132}
133
134impl IdentityStrategy {
135    /**
136     * Initialize an identity from the given strategy. If a stored identity is found in the database,
137     * it will return that identity.
138     *
139     * If a stored identity is found, it will validate that the inbox_id of the stored identity matches
140     * the inbox_id configured on the strategy.
141     *
142     **/
143    #[tracing::instrument(level = "trace", skip_all)]
144    pub(crate) async fn initialize_identity<ApiClient: XmtpApi, S: XmtpMlsStorageProvider>(
145        self,
146        api_client: &ApiClientWrapper<ApiClient>,
147        mls_storage: &S,
148        scw_signature_verifier: impl SmartContractSignatureVerifier,
149    ) -> Result<Identity, IdentityError> {
150        use IdentityStrategy::*;
151
152        info!("Initializing identity");
153        let stored_identity: Option<Identity> = mls_storage
154            .db()
155            .fetch(&())?
156            .map(|i: StoredIdentity| i.try_into())
157            .transpose()?;
158
159        let strategy = match &self {
160            CreateIfNotFound { .. } => "create_if_not_found",
161            CreateForIdentifier { .. } => "create_for_identifier",
162            CachedOnly => "cached_only",
163            #[cfg(any(test, feature = "test-utils"))]
164            ExternalIdentity(_) => "external_identity",
165        };
166        debug!(
167            strategy,
168            stored_identity_present = stored_identity.is_some(),
169            "identity strategy"
170        );
171        match self.with_inbox(api_client).await? {
172            CachedOnly => stored_identity.ok_or(IdentityError::RequiredIdentityNotFound),
173            CreateForIdentifier { .. } => unreachable!("with_inbox replaces the strategy"),
174            CreateIfNotFound {
175                inbox_id,
176                identifier,
177                nonce,
178            } => {
179                if let Some(stored_identity) = stored_identity {
180                    tracing::debug!(
181                        installation_id =
182                            stored_identity.installation_keys.public_bytes().snippet(),
183                        inbox_id = stored_identity.inbox_id,
184                        "Found existing identity in store"
185                    );
186                    if inbox_id != stored_identity.inbox_id {
187                        return Err(IdentityError::InboxIdMismatch {
188                            id: inbox_id.clone(),
189                            stored: stored_identity.inbox_id,
190                        });
191                    }
192
193                    Ok(stored_identity)
194                } else {
195                    Identity::new(
196                        inbox_id,
197                        identifier,
198                        nonce,
199                        api_client,
200                        mls_storage,
201                        &scw_signature_verifier,
202                    )
203                    .await
204                }
205            }
206            #[cfg(any(test, feature = "test-utils"))]
207            ExternalIdentity(identity) => Ok(identity),
208        }
209    }
210}
211
212#[derive(Debug, Error, ErrorCode)]
213pub enum IdentityError {
214    /// Credential serialization error.
215    ///
216    /// Failed to encode MLS credential. Not retryable.
217    #[error(transparent)]
218    CredentialSerialization(#[from] prost::EncodeError),
219    /// Decode error.
220    ///
221    /// Protobuf decoding failed. Not retryable.
222    #[error(transparent)]
223    Decode(#[from] prost::DecodeError),
224    /// Installation not found.
225    ///
226    /// Installation ID not found in network association state. Not retryable.
227    #[error("installation not found: {0}")]
228    InstallationIdNotFound(String),
229    #[error(transparent)]
230    #[error_code(inherit)]
231    SignatureRequestBuilder(#[from] SignatureRequestError),
232    #[error(transparent)]
233    #[error_code(inherit)]
234    Signature(#[from] xmtp_id::associations::SignatureError),
235    /// Basic credential error.
236    ///
237    /// MLS basic credential validation failed. Not retryable.
238    #[error(transparent)]
239    BasicCredential(#[from] BasicCredentialError),
240    /// Uninitialized identity.
241    ///
242    /// Identity not yet initialized. Not retryable.
243    #[error("Uninitialized identity")]
244    UninitializedIdentity,
245    /// Installation key error.
246    ///
247    /// Problem with installation key. Not retryable.
248    #[error("Installation key {0}")]
249    InstallationKey(String),
250    /// Crypto error.
251    ///
252    /// Cryptographic operation failed. Not retryable.
253    #[error(transparent)]
254    Crypto(#[from] CryptoError),
255    /// OpenMLS error.
256    ///
257    /// OpenMLS library error. Not retryable.
258    #[error(transparent)]
259    OpenMls(#[from] openmls::prelude::Error),
260    #[error(transparent)]
261    #[error_code(inherit)]
262    StorageError(#[from] xmtp_db::StorageError),
263    #[error(transparent)]
264    #[error_code(inherit)]
265    OpenMlsStorageError(#[from] SqlKeyStoreError),
266    /// Key package generation error.
267    ///
268    /// Failed to generate MLS key package. Not retryable.
269    #[error(transparent)]
270    KeyPackageGenerationError(#[from] openmls::key_packages::errors::KeyPackageNewError),
271    #[error(transparent)]
272    #[error_code(inherit)]
273    KeyPackageVerificationError(#[from] KeyPackageVerificationError),
274    /// Inbox ID mismatch.
275    ///
276    /// Associated InboxID does not match stored value. Not retryable.
277    #[error("The InboxID {id}, associated does not match the stored InboxId {stored}.")]
278    InboxIdMismatch { id: InboxId, stored: InboxId },
279    /// Identifier not in inbox.
280    ///
281    /// The identifier does not belong to the inbox of the stored identity. Not retryable.
282    #[error("The identifier does not belong to the stored InboxId {inbox_id}.")]
283    IdentifierNotInInbox { inbox_id: InboxId },
284    /// No associated Inbox ID.
285    ///
286    /// Address has no associated InboxID. Not retryable.
287    #[error("The address {0} has no associated InboxID")]
288    NoAssociatedInboxId(String),
289    /// Required identity not found.
290    ///
291    /// Identity was not found in cache. Not retryable.
292    #[error("Required identity was not found in cache.")]
293    RequiredIdentityNotFound,
294    /// New identity creation error.
295    ///
296    /// Error creating a new identity. Not retryable.
297    #[error("error creating new identity: {0}")]
298    NewIdentity(String),
299    #[error(transparent)]
300    #[error_code(inherit)]
301    Association(#[from] AssociationError),
302    /// Signer error.
303    ///
304    /// Cryptographic signer failed. Not retryable.
305    #[error(transparent)]
306    Signer(#[from] xmtp_cryptography::SignerError),
307    #[error(transparent)]
308    #[error_code(inherit)]
309    ApiClient(#[from] xmtp_api::ApiError),
310    /// Identity publication failed. Retryability depends on the cause.
311    #[error(transparent)]
312    IdentityUpdate(#[from] crate::identity_updates::IdentityUpdateError),
313    #[error(transparent)]
314    #[error_code(inherit)]
315    AddressValidation(#[from] IdentifierValidationError),
316    #[error(transparent)]
317    #[error_code(inherit)]
318    Db(#[from] xmtp_db::ConnectionError),
319    /// Too many installations.
320    ///
321    /// InboxID has reached max installation count. Not retryable.
322    #[error(
323        "Cannot register a new installation because the InboxID {inbox_id} has already registered {count}/{max} installations. Please revoke existing installations first."
324    )]
325    TooManyInstallations {
326        inbox_id: String,
327        count: usize,
328        max: usize,
329    },
330    #[error(transparent)]
331    #[error_code(inherit)]
332    GeneratePostQuantumKey(#[from] GeneratePostQuantumKeyError),
333    /// Invalid extension error.
334    ///
335    /// MLS extension validation failed. Not retryable.
336    #[error(transparent)]
337    InvalidExtension(#[from] openmls::prelude::InvalidExtensionError),
338    /// Missing PQ public key.
339    ///
340    /// Post-quantum public key not found. Not retryable.
341    #[error("Missing post quantum public key")]
342    MissingPostQuantumPublicKey,
343    /// Bincode serialization error.
344    ///
345    /// Binary serialization failed. Not retryable.
346    #[error("Bincode serialization error")]
347    Bincode,
348    /// Uninitialized field.
349    ///
350    /// Builder field not initialized. Not retryable.
351    #[error(transparent)]
352    UninitializedField(#[from] derive_builder::UninitializedFieldError),
353}
354
355impl From<xmtp_db::diesel::result::Error> for IdentityError {
356    fn from(error: xmtp_db::diesel::result::Error) -> Self {
357        Self::StorageError(error.into())
358    }
359}
360
361impl NeedsDbReconnect for IdentityError {
362    fn needs_db_reconnect(&self) -> bool {
363        match self {
364            Self::StorageError(s) => s.db_needs_connection(),
365            Self::Db(c) => c.db_needs_connection(),
366            // Keystore ops (rotate/delete paths) hit the same pool.
367            Self::OpenMlsStorageError(SqlKeyStoreError::Connection(c)) => c.db_needs_connection(),
368            _ => false,
369        }
370    }
371}
372
373impl RetryableError for IdentityError {
374    fn is_retryable(&self) -> bool {
375        match self {
376            Self::ApiClient(err) => retryable!(err),
377            Self::StorageError(err) => retryable!(err),
378            Self::OpenMlsStorageError(err) => retryable!(err),
379            _ => false,
380        }
381    }
382}
383
384#[derive(Debug)]
385pub struct Identity {
386    pub(crate) inbox_id: InboxId,
387    pub(crate) installation_keys: XmtpInstallationCredential,
388    pub(crate) credential: OpenMlsCredential,
389    pub(crate) signature_request: Option<SignatureRequest>,
390    pub(crate) is_ready: AtomicBool,
391}
392
393impl Clone for Identity {
394    fn clone(&self) -> Self {
395        Self {
396            inbox_id: self.inbox_id.clone(),
397            installation_keys: self.installation_keys.clone(),
398            credential: self.credential.clone(),
399            signature_request: self.signature_request(),
400            is_ready: AtomicBool::new(self.is_ready.load(Ordering::SeqCst)),
401        }
402    }
403}
404
405impl TryFrom<&Identity> for StoredIdentity {
406    type Error = StorageError;
407
408    fn try_from(identity: &Identity) -> Result<Self, Self::Error> {
409        StoredIdentity::builder()
410            .inbox_id(identity.inbox_id.clone())
411            .installation_keys(xmtp_db::db_serialize(&identity.installation_keys)?)
412            .credential_bytes(xmtp_db::db_serialize(&identity.credential())?)
413            .next_key_package_rotation_ns(now_ns() + KEY_PACKAGE_ROTATION_INTERVAL_NS)
414            .build()
415    }
416}
417
418impl TryFrom<StoredIdentity> for Identity {
419    type Error = StorageError;
420
421    fn try_from(identity: StoredIdentity) -> Result<Self, Self::Error> {
422        Ok(Identity {
423            inbox_id: identity.inbox_id.clone(),
424            installation_keys: xmtp_db::db_deserialize(&identity.installation_keys)?,
425            credential: xmtp_db::db_deserialize(&identity.credential_bytes)?,
426            signature_request: None,
427            is_ready: AtomicBool::new(true),
428        })
429    }
430}
431
432pub(crate) struct NewKeyPackageResult {
433    pub(crate) key_package: KeyPackage,
434    pub(crate) pq_pub_key: Option<Vec<u8>>,
435}
436
437impl Identity {
438    /// Create a new [Identity] instance.
439    ///
440    /// If the address is already associated with an inbox_id, the existing inbox_id will be used.
441    /// If it is not, a new inbox_id is generated from the identifier and nonce.
442    /// Either way, the caller must sign with their wallet.
443    #[tracing::instrument(level = "trace", skip_all)]
444    pub(crate) async fn new<ApiClient: XmtpApi, S: XmtpMlsStorageProvider>(
445        inbox_id: InboxId,
446        identifier: Identifier,
447        nonce: u64,
448        api_client: &ApiClientWrapper<ApiClient>,
449        mls_storage: &S,
450        scw_signature_verifier: impl SmartContractSignatureVerifier,
451    ) -> Result<Self, IdentityError> {
452        // check if address is already associated with an inbox_id
453        let inbox_ids = api_client
454            .get_inbox_ids(vec![identifier.clone().into()])
455            .await?;
456        let associated_inbox_id = inbox_ids.first().and_then(Option::as_ref);
457        let installation_keys = XmtpInstallationCredential::new();
458
459        if let Some(associated_inbox_id) = associated_inbox_id {
460            // If an inbox is associated with address, we'd use it to create Identity and ignore the nonce.
461            // We would need a signature from user's wallet.
462            if *associated_inbox_id != inbox_id {
463                return Err(IdentityError::NewIdentity("Inbox ID mismatch".to_string()));
464            }
465
466            // get sequence_id from identity updates and loaded into the DB
467            load_identity_updates(
468                api_client,
469                &mls_storage.db(),
470                &[associated_inbox_id.as_str()],
471            )
472            .await
473            .map_err(|e| {
474                IdentityError::NewIdentity(format!("Failed to load identity updates: {e}"))
475            })?;
476
477            let state = get_association_state_with_verifier(
478                &mls_storage.db(),
479                &inbox_id,
480                None,
481                &scw_signature_verifier,
482            )
483            .await
484            .map_err(|err| {
485                IdentityError::NewIdentity(format!("Error resolving identity state: {}", err))
486            })?;
487
488            // The deployment sets the ceiling; the wrapper carries the
489            // snapshot resolved before any identity work ran.
490            let max_installations = api_client.configuration().mls.max_installations_per_inbox;
491            let current_installation_count = state.installation_ids().len();
492            // implements: CONF-044
493            if current_installation_count >= max_installations {
494                return Err(IdentityError::TooManyInstallations {
495                    inbox_id: associated_inbox_id.clone(),
496                    count: current_installation_count,
497                    max: max_installations,
498                });
499            }
500
501            let builder = SignatureRequestBuilder::new(associated_inbox_id.clone());
502            let mut signature_request = builder
503                .add_association(
504                    MemberIdentifier::installation(installation_keys.public_slice().to_vec()),
505                    identifier.clone().into(),
506                )
507                .build();
508
509            let signature = installation_keys
510                .credential_sign::<InstallationKeyContext>(signature_request.signature_text())?;
511            signature_request
512                .add_signature(
513                    UnverifiedSignature::new_installation_key(
514                        signature,
515                        installation_keys.verifying_key(),
516                    ),
517                    &scw_signature_verifier,
518                )
519                .await?;
520
521            let identity = Self {
522                inbox_id: associated_inbox_id.clone(),
523                installation_keys,
524                credential: create_credential(associated_inbox_id.clone())?,
525                signature_request: Some(signature_request),
526                is_ready: AtomicBool::new(false),
527            };
528
529            Ok(identity)
530        } else {
531            let generated_inbox_id = identifier.inbox_id(nonce)?;
532            if inbox_id != generated_inbox_id {
533                return Err(IdentityError::NewIdentity(
534                    "Inbox ID doesn't match nonce & address".to_string(),
535                ));
536            }
537            let mut builder = SignatureRequestBuilder::new(inbox_id.clone());
538            builder = builder.create_inbox(identifier.clone(), nonce);
539
540            let mut signature_request = builder
541                .add_association(
542                    MemberIdentifier::installation(installation_keys.public_slice().to_vec()),
543                    identifier.clone().into(),
544                )
545                .build();
546
547            // We can pre-sign the request with an installation key signature, since we have access to the key
548            let sig = installation_keys
549                .credential_sign::<InstallationKeyContext>(signature_request.signature_text())?;
550            signature_request
551                .add_signature(
552                    UnverifiedSignature::new_installation_key(
553                        sig,
554                        installation_keys.verifying_key(),
555                    ),
556                    &scw_signature_verifier,
557                )
558                .await?;
559
560            let identity = Self {
561                inbox_id: inbox_id.clone(),
562                installation_keys,
563                credential: create_credential(inbox_id.clone())?,
564                signature_request: Some(signature_request),
565                is_ready: AtomicBool::new(false),
566            };
567
568            Ok(identity)
569        }
570    }
571
572    pub fn inbox_id(&self) -> InboxIdRef<'_> {
573        &self.inbox_id
574    }
575
576    pub fn installation_id(&self) -> InstallationId {
577        (*self.installation_keys.public_bytes()).into()
578    }
579
580    pub fn sequence_id<C>(&self, conn: &DbConnection<C>) -> Result<i64, xmtp_db::ConnectionError>
581    where
582        C: ConnectionExt,
583    {
584        conn.get_latest_sequence_id_for_inbox(self.inbox_id.as_str())
585    }
586
587    pub fn is_ready(&self) -> bool {
588        self.is_ready.load(Ordering::SeqCst)
589    }
590
591    pub(crate) fn set_ready(&self) {
592        self.is_ready.store(true, Ordering::SeqCst)
593    }
594
595    pub fn signature_request(&self) -> Option<SignatureRequest> {
596        self.signature_request.clone()
597    }
598
599    pub fn credential(&self) -> OpenMlsCredential {
600        self.credential.clone()
601    }
602
603    /**
604     * Sign the given text with the installation private key.
605     */
606    pub(crate) fn sign_identity_update<Text: AsRef<str>>(
607        &self,
608        text: Text,
609    ) -> Result<Vec<u8>, IdentityError> {
610        self.installation_keys
611            .credential_sign::<InstallationKeyContext>(text)
612            .map_err(Into::into)
613    }
614
615    pub fn sign_with_public_context(
616        &self,
617        text: impl AsRef<str>,
618    ) -> Result<Vec<u8>, IdentityError> {
619        self.installation_keys
620            .credential_sign::<PublicContext>(text)
621            .map_err(Into::into)
622    }
623
624    /// Generate a new key package and store the associated keys in the database.
625    #[tracing::instrument(level = "trace", skip_all)]
626    pub(crate) fn new_key_package(
627        &self,
628        provider: &impl MlsProviderExt,
629        include_post_quantum: bool,
630    ) -> Result<NewKeyPackageResult, IdentityError> {
631        XmtpKeyPackage::builder()
632            .inbox_id(self.inbox_id())
633            .credential(self.credential())
634            .installation_keys(self.installation_keys.clone())
635            .build(provider, include_post_quantum)
636    }
637
638    /// Store fresh key material before upload, then record its publication receipt.
639    /// Receipt order controls retirement. Unknown publications stay available.
640    /// Receipt bookkeeping and the next rotation deadline commit together.
641    #[tracing::instrument(level = "trace", skip_all)]
642    pub(crate) async fn rotate_and_upload_key_package<
643        ApiClient: XmtpApi,
644        S: XmtpMlsStorageProvider,
645    >(
646        &self,
647        api_client: &ApiClientWrapper<ApiClient>,
648        mls_storage: &S,
649        include_post_quantum: bool,
650    ) -> Result<(), IdentityError> {
651        tracing::info!("Start rotating keys and uploading the new key package");
652
653        // Generate and store key package locally
654        let (kp_bytes, history_id) =
655            self.generate_and_store_key_package(mls_storage, include_post_quantum)?;
656
657        // Upload to network
658        match api_client.upload_key_package(kp_bytes).await {
659            Ok(meta) => {
660                let published_cursor = meta.cursor.filter(|cursor| cursor.sequence_id > 0).ok_or(
661                    xmtp_api::ApiError::InvalidResponse("key package publish cursor"),
662                )?;
663                // Backend publication order can differ from local generation order.
664                crate::state_tx::state_write(mls_storage, |tx| {
665                    let storage = tx.storage();
666                    storage.db().record_key_package_publication(
667                        history_id,
668                        xmtp_proto::types::Cursor(published_cursor.sequence_id),
669                    )?;
670                    storage
671                        .db()
672                        .reset_key_package_rotation_queue(KEY_PACKAGE_ROTATION_INTERVAL_NS)?;
673                    Ok::<_, StorageError>(Continue(()))
674                })
675                .map(TransactionOutcome::into_continued)?;
676                Ok(())
677            }
678            Err(err) => Err(IdentityError::ApiClient(err)),
679        }
680    }
681
682    /// Store key material and its history row in one state transaction.
683    /// Return bytes for upload and the history ID for its later receipt.
684    #[tracing::instrument(level = "trace", skip_all)]
685    pub(crate) fn generate_and_store_key_package<S: XmtpMlsStorageProvider>(
686        &self,
687        mls_storage: &S,
688        include_post_quantum: bool,
689    ) -> Result<(Vec<u8>, i32), IdentityError> {
690        crate::state_tx::state_write(mls_storage, |tx| {
691            let storage = tx.storage();
692            let provider = XmtpOpenMlsProviderRef::new(&storage);
693            let NewKeyPackageResult {
694                key_package: kp,
695                pq_pub_key,
696            } = self.new_key_package(&provider, include_post_quantum)?;
697            let hash_ref = serialize_key_package_hash_ref(&kp, &provider)?;
698            let history_id = storage
699                .db()
700                .store_key_package_history_entry(hash_ref, pq_pub_key)?
701                .id;
702            let kp_bytes = kp.tls_serialize_detached()?;
703            Ok::<_, IdentityError>(Continue((kp_bytes, history_id)))
704        })
705        .map(TransactionOutcome::into_continued)
706    }
707}
708
709#[cfg(any(test, feature = "test-utils"))]
710tokio::task_local! {
711    pub static ENABLE_WELCOME_POINTERS: bool;
712}
713
714#[derive(Builder, Debug)]
715#[builder(build_fn(error = "IdentityError", name = "inner_build", private))]
716pub struct XmtpKeyPackage {
717    #[builder(setter(into))]
718    inbox_id: String,
719    #[builder(setter(into))]
720    credential: OpenMlsCredential,
721    #[builder(setter(into))]
722    installation_keys: XmtpInstallationCredential,
723}
724
725impl XmtpKeyPackage {
726    pub(crate) fn builder() -> XmtpKeyPackageBuilder {
727        XmtpKeyPackageBuilder::default()
728    }
729}
730
731impl XmtpKeyPackageBuilder {
732    pub(crate) fn build(
733        &mut self,
734        provider: &impl MlsProviderExt,
735        include_post_quantum: bool,
736    ) -> Result<NewKeyPackageResult, IdentityError> {
737        let this = self.inner_build()?;
738        #[allow(unused_mut)]
739        let mut options = xmtp_id::key_package::KeyPackageOptions {
740            include_post_quantum,
741            ..Default::default()
742        };
743        #[cfg(any(test, feature = "test-utils"))]
744        {
745            options.welcome_pointers = ENABLE_WELCOME_POINTERS.try_with(|v| *v).unwrap_or(true);
746            options.lifetime =
747                Some(crate::utils::test_mocks_helpers::maybe_mock_package_lifetime());
748        }
749        let generated = xmtp_id::key_package::build_key_package(
750            &this.inbox_id,
751            this.credential,
752            &this.installation_keys,
753            provider,
754            options,
755        )
756        .map_err(|error| match error {
757            xmtp_id::key_package::KeyPackageConstructionError::Generation(e) => {
758                IdentityError::KeyPackageGenerationError(e)
759            }
760            xmtp_id::key_package::KeyPackageConstructionError::InvalidExtension(e) => {
761                IdentityError::InvalidExtension(e)
762            }
763            xmtp_id::key_package::KeyPackageConstructionError::Encode(e) => {
764                IdentityError::CredentialSerialization(e)
765            }
766            xmtp_id::key_package::KeyPackageConstructionError::PostQuantum(e) => {
767                IdentityError::GeneratePostQuantumKey(e)
768            }
769        })?;
770        store_key_package_references(
771            provider,
772            generated.bundle.key_package(),
773            &generated.post_quantum_keypair,
774        )?;
775        Ok(NewKeyPackageResult {
776            key_package: generated.bundle.key_package().clone(),
777            pq_pub_key: generated.post_quantum_keypair.map(|kp| kp.public),
778        })
779    }
780}
781
782/// Serialize the key package hash ref to a bincode friendly format that is compatible with `read` in the KeyStore
783pub(crate) fn serialize_key_package_hash_ref(
784    kp: &KeyPackage,
785    provider: &impl MlsProviderExt,
786) -> Result<Vec<u8>, IdentityError> {
787    let key_package_hash_ref = kp
788        .hash_ref(provider.crypto())
789        .map_err(|_| IdentityError::UninitializedIdentity)?;
790    let serialized = bincode::serialize(&key_package_hash_ref)
791        .map_err(|_| IdentityError::UninitializedIdentity)?;
792
793    Ok(serialized)
794}
795
796// Takes a post quantum public key and returns the key used to store it in the key package references table
797pub(crate) fn pq_key_package_references_key(raw_pub_key: &[u8]) -> Result<Vec<u8>, IdentityError> {
798    Ok(raw_pub_key.tls_serialize_detached()?)
799}
800
801pub(crate) fn deserialize_key_package_hash_ref(
802    hash_ref: &[u8],
803) -> Result<HashReference, IdentityError> {
804    let key_package_hash_ref: HashReference =
805        bincode::deserialize(hash_ref).map_err(|_| IdentityError::UninitializedIdentity)?;
806
807    Ok(key_package_hash_ref)
808}
809
810pub(crate) fn create_credential(
811    inbox_id: impl AsRef<str>,
812) -> Result<OpenMlsCredential, IdentityError> {
813    Ok(xmtp_id::key_package::create_credential(inbox_id))
814}
815
816pub fn parse_credential(credential_bytes: &[u8]) -> Result<InboxId, IdentityError> {
817    Ok(xmtp_id::key_package::parse_credential(credential_bytes)?)
818}
819
820pub fn build_post_quantum_public_key_extension(
821    public_key: &[u8],
822) -> Result<Extension, IdentityError> {
823    Ok(xmtp_id::key_package::build_post_quantum_public_key_extension(public_key)?)
824}
825
826// Store the hash reference, keyed with both the public init key and the post quantum init key.
827// This is needed to get to the private key when decrypting welcome messages.
828// Both the Curve25519 and the Post Quantum keys hold a hash reference to the key package.
829// If a post quantum key is present, we also have a pointer from the key package hash ref -> the post quantum private key.
830pub(crate) fn store_key_package_references(
831    provider: &impl MlsProviderExt,
832    kp: &KeyPackage,
833    // The post quantum init key for the key package used for Post Quantum Welcome Wrapper encryption
834    post_quantum_keypair: &Option<HpkeKeyPair>,
835) -> Result<(), IdentityError> {
836    // For dumb legacy reasons that are probably my fault, we keep the key package references
837    // keyed by the TLS serialized public init key instead of the slice version.
838    let public_init_key = kp.hpke_init_key().tls_serialize_detached()?;
839
840    let hash_ref = serialize_key_package_hash_ref(kp, provider)?;
841    let storage = provider.key_store();
842    // Write the normal init key to the key package references
843    storage.write(KEY_PACKAGE_REFERENCES, &public_init_key, &hash_ref)?;
844
845    if let Some(post_quantum_keypair) = post_quantum_keypair {
846        let post_quantum_public_key = pq_key_package_references_key(&post_quantum_keypair.public)?;
847        // We need to store this in a bincode friendly format so that `read` will work later.
848        // TODO:(nm) review whether this breaks the Zeroize guarantees
849        let post_quantum_private_key = bincode::serialize(&post_quantum_keypair.private.to_vec())
850            .map_err(|_| IdentityError::Bincode)?;
851
852        // Write the post quantum wrapper encryption public key to the key package references
853        storage.write(KEY_PACKAGE_REFERENCES, &post_quantum_public_key, &hash_ref)?;
854
855        storage.write(
856            KEY_PACKAGE_WRAPPER_PRIVATE_KEY,
857            &hash_ref,
858            &post_quantum_private_key,
859        )?;
860    }
861
862    Ok(())
863}
864
865#[cfg(test)]
866mod tests {
867    use crate::context::XmtpSharedContext;
868    use crate::{
869        builder::ClientBuilder,
870        identity::{pq_key_package_references_key, serialize_key_package_hash_ref},
871        utils::FullXmtpClient,
872    };
873    use xmtp_id::key_package::VerifiedKeyPackageV2;
874
875    use openmls::prelude::{KeyPackageBundle, KeyPackageRef};
876    use openmls_traits::{OpenMlsProvider, storage::StorageProvider};
877    use tls_codec::Serialize;
878    use xmtp_cryptography::utils::generate_local_wallet;
879    use xmtp_db::XmtpMlsStorageProvider;
880    use xmtp_db::XmtpOpenMlsProviderRef;
881    use xmtp_db::{
882        MlsProviderExt,
883        group::{ConversationType, GroupQueryArgs},
884        sql_key_store::{KEY_PACKAGE_REFERENCES, KEY_PACKAGE_WRAPPER_PRIVATE_KEY},
885    };
886    use xmtp_id::key_package::WrapperAlgorithm;
887    use xmtp_mls_common::group::DMMetadataOptions;
888
889    #[cfg(not(target_arch = "wasm32"))]
890    #[xmtp_common::test(unwrap_try = true)]
891    async fn initialize_identity_debug_log_reports_strategy() {
892        use super::IdentityStrategy;
893        use openmls::credentials::{Credential, CredentialType};
894        use std::sync::atomic::AtomicBool;
895        use tracing::instrument::WithSubscriber;
896        use xmtp_api::ApiClientWrapper;
897        use xmtp_api_backend::MockBackendClient;
898        use xmtp_common::{Retry, rand_vec};
899        use xmtp_cryptography::XmtpInstallationCredential;
900        use xmtp_db::{Store, XmtpTestDb, identity::StoredIdentity, sql_key_store::SqlKeyStore};
901        use xmtp_id::associations::test_utils::{
902            MockSmartContractSignatureVerifier, WalletTestExt,
903        };
904        use xmtp_logging::{Level, test_logging::LogCapture};
905
906        let store = xmtp_db::TestDb::create_persistent_store(None).await;
907        let identifier = generate_local_wallet().identifier();
908        let inbox_id = identifier.inbox_id(0).unwrap();
909        let stored: StoredIdentity = (&super::Identity {
910            inbox_id: inbox_id.clone(),
911            installation_keys: XmtpInstallationCredential::new(),
912            credential: Credential::new(CredentialType::Basic, rand_vec::<24>()),
913            signature_request: None,
914            is_ready: AtomicBool::new(true),
915        })
916            .try_into()
917            .unwrap();
918        stored.store(&store.conn()).unwrap();
919
920        let strategy = IdentityStrategy::new(inbox_id.clone(), identifier, 0);
921        let api = ApiClientWrapper::new(MockBackendClient::new(), Retry::default());
922        let verifier = MockSmartContractSignatureVerifier::new(true);
923        let capture = LogCapture::new(Level::Debug);
924        let loaded = strategy
925            .initialize_identity(&api, &SqlKeyStore::new(&store.db()), &verifier)
926            .with_subscriber(capture.dispatch())
927            .await
928            .unwrap();
929
930        let output = capture.output();
931        assert_eq!(loaded.inbox_id, inbox_id);
932        assert!(output.contains("identity strategy"), "{output}");
933        assert!(
934            output.contains("\"strategy\":\"create_if_not_found\""),
935            "{output}"
936        );
937        assert!(
938            output.contains("\"stored_identity_present\":true"),
939            "{output}"
940        );
941    }
942
943    async fn get_key_package_from_network(client: &FullXmtpClient) -> VerifiedKeyPackageV2 {
944        let mut kp_mapping = client
945            .get_key_packages_for_installation_ids(vec![client.installation_public_key().to_vec()])
946            .await
947            .unwrap();
948
949        kp_mapping
950            .remove(client.installation_public_key().as_slice())
951            .unwrap()
952            .unwrap()
953    }
954
955    async fn get_latest_welcome(client: &FullXmtpClient) -> xmtp_proto::types::WelcomeMessage {
956        let welcomes = client
957            .context
958            .api()
959            .query_welcome_messages(client.context.installation_id())
960            .await
961            .unwrap();
962
963        welcomes[0].clone()
964    }
965
966    /// Look up the key package hash ref by public init key
967    fn get_hash_ref(provider: &impl MlsProviderExt, pub_key: &[u8]) -> Option<KeyPackageRef> {
968        provider
969            .key_store()
970            .read(KEY_PACKAGE_REFERENCES, pub_key)
971            .unwrap()
972    }
973
974    fn get_pq_private_key(provider: &impl MlsProviderExt, hash_ref: &[u8]) -> Option<Vec<u8>> {
975        let val: Option<Vec<u8>> = provider
976            .key_store()
977            .read::<Vec<u8>>(KEY_PACKAGE_WRAPPER_PRIVATE_KEY, hash_ref)
978            .unwrap();
979
980        val
981    }
982
983    #[xmtp_common::test]
984    async fn ensure_pq_keys_are_deleted() {
985        let client = ClientBuilder::new_test_client(&generate_local_wallet()).await;
986        let storage = client.context.mls_storage();
987        let provider = XmtpOpenMlsProviderRef::new(storage);
988
989        // As long as we have `config::CREATE_PQ_KEY_PACKAGE_EXTENSION` set to false, we need to do this step to force a PQ key package to be created
990        let api_client = client.context.api();
991        client
992            .identity()
993            .rotate_and_upload_key_package(api_client, storage, true)
994            .await
995            .unwrap();
996
997        // Get the key package back from the network
998        let starting_key_package = get_key_package_from_network(&client).await;
999        let starting_init_key = starting_key_package
1000            .inner
1001            .hpke_init_key()
1002            .tls_serialize_detached()
1003            .unwrap();
1004
1005        // Make sure we can find the init key
1006        let init_key_hash_ref = get_hash_ref(&provider, &starting_init_key);
1007        assert!(init_key_hash_ref.is_some());
1008
1009        // Make sure we can find the post quantum public key
1010        let pq_public_key = starting_key_package.wrapper_encryption().unwrap();
1011        assert!(pq_public_key.is_some());
1012
1013        let pq_public_key_bytes = pq_public_key.unwrap().pub_key_bytes;
1014        let pq_hash_ref = get_hash_ref(
1015            &provider,
1016            &pq_key_package_references_key(&pq_public_key_bytes).unwrap(),
1017        );
1018        assert!(pq_hash_ref.is_some());
1019        let pq_hash_ref_inner = pq_hash_ref.unwrap();
1020
1021        // Make sure we can find the key package based on the post quantum public key
1022        let key_package_bundle: KeyPackageBundle = provider
1023            .storage()
1024            .key_package(&pq_hash_ref_inner)
1025            .unwrap()
1026            .unwrap();
1027
1028        // Make sure we can find the private key based on the init key
1029        let serialized_hash_ref = bincode::serialize(&init_key_hash_ref.unwrap()).unwrap();
1030        let pq_private_key = get_pq_private_key(&provider, &serialized_hash_ref);
1031        assert!(pq_private_key.is_some());
1032
1033        // Now rotate the key package
1034        client.rotate_and_upload_key_package().await.unwrap();
1035
1036        // Force deletion of the key package, even though it hasn't expired yet
1037        let serialized_key_package_hash_ref =
1038            serialize_key_package_hash_ref(key_package_bundle.key_package(), &provider).unwrap();
1039        crate::worker::key_package_maintenance::delete_key_package(
1040            &client.context,
1041            serialized_key_package_hash_ref,
1042            Some(pq_public_key_bytes.clone()),
1043        )
1044        .unwrap();
1045
1046        // Now test to see if the private keys are deleted by doing the same steps as above
1047        let pq_hash_ref = get_hash_ref(
1048            &provider,
1049            &pq_key_package_references_key(&pq_public_key_bytes).unwrap(),
1050        );
1051        assert!(pq_hash_ref.is_none());
1052
1053        let pq_private_key = get_pq_private_key(&provider, &serialized_hash_ref);
1054        assert!(pq_private_key.is_none());
1055
1056        let key_package_from_db: Option<KeyPackageBundle> =
1057            provider.storage().key_package(&pq_hash_ref_inner).unwrap();
1058        assert!(key_package_from_db.is_none());
1059    }
1060
1061    #[test]
1062    fn test_generate_post_quantum_key_error_codes() {
1063        use super::GeneratePostQuantumKeyError;
1064        use openmls_traits::types::CryptoError;
1065        use xmtp_common::ErrorCode;
1066
1067        // Test Crypto variant
1068        let crypto_err = GeneratePostQuantumKeyError::Crypto(CryptoError::CryptoLibraryError);
1069        assert_eq!(
1070            crypto_err.error_code(),
1071            "GeneratePostQuantumKeyError::Crypto"
1072        );
1073    }
1074
1075    #[test]
1076    fn test_identity_error_codes() {
1077        use super::IdentityError;
1078        use xmtp_common::ErrorCode;
1079
1080        // Test simple variants
1081        let err = IdentityError::UninitializedIdentity;
1082        assert_eq!(err.error_code(), "IdentityError::UninitializedIdentity");
1083
1084        let err = IdentityError::RequiredIdentityNotFound;
1085        assert_eq!(err.error_code(), "IdentityError::RequiredIdentityNotFound");
1086
1087        let err = IdentityError::Bincode;
1088        assert_eq!(err.error_code(), "IdentityError::Bincode");
1089
1090        let err = IdentityError::MissingPostQuantumPublicKey;
1091        assert_eq!(
1092            err.error_code(),
1093            "IdentityError::MissingPostQuantumPublicKey"
1094        );
1095
1096        // Test variants with data
1097        let err = IdentityError::InstallationIdNotFound("test".to_string());
1098        assert_eq!(err.error_code(), "IdentityError::InstallationIdNotFound");
1099
1100        let err = IdentityError::InstallationKey("test".to_string());
1101        assert_eq!(err.error_code(), "IdentityError::InstallationKey");
1102
1103        let err = IdentityError::NewIdentity("test".to_string());
1104        assert_eq!(err.error_code(), "IdentityError::NewIdentity");
1105
1106        let err = IdentityError::TooManyInstallations {
1107            inbox_id: "test".to_string(),
1108            count: 10,
1109            max: 5,
1110        };
1111        assert_eq!(err.error_code(), "IdentityError::TooManyInstallations");
1112
1113        let err = IdentityError::InboxIdMismatch {
1114            id: "id1".to_string(),
1115            stored: "id2".to_string(),
1116        };
1117        assert_eq!(err.error_code(), "IdentityError::InboxIdMismatch");
1118
1119        let err = IdentityError::NoAssociatedInboxId("addr".to_string());
1120        assert_eq!(err.error_code(), "IdentityError::NoAssociatedInboxId");
1121    }
1122
1123    #[test]
1124    fn test_identity_error_inherited_codes() {
1125        use super::IdentityError;
1126        use xmtp_common::ErrorCode;
1127        use xmtp_db::{NotFound, StorageError};
1128
1129        // Test inherited error codes
1130        let storage_err = StorageError::NotFound(NotFound::MessageById(vec![1, 2, 3]));
1131        let err = IdentityError::StorageError(storage_err);
1132        assert_eq!(err.error_code(), "StorageError::NotFound");
1133    }
1134
1135    #[xmtp_common::test]
1136    async fn post_quantum_interop() {
1137        for [amal_has_pq, bola_has_pq] in
1138            [[true, false], [false, true], [true, true], [false, false]]
1139        {
1140            let amal = ClientBuilder::new_test_client(&generate_local_wallet()).await;
1141            let amal_api = amal.context.api();
1142            let amal_mls = amal.context.mls_storage();
1143
1144            let bola = ClientBuilder::new_test_client(&generate_local_wallet()).await;
1145            let bola_api = bola.context.api();
1146            let bola_mls = bola.context.mls_storage();
1147
1148            // Give amal a post quantum key package and bola a legacy key package
1149            amal.identity()
1150                .rotate_and_upload_key_package(amal_api, amal_mls, amal_has_pq)
1151                .await
1152                .unwrap();
1153            bola.identity()
1154                .rotate_and_upload_key_package(bola_api, bola_mls, bola_has_pq)
1155                .await
1156                .unwrap();
1157
1158            // Create a DM from Amal -> Bola
1159            // This should use Bola's XWingMLKEM512 key package
1160            amal.find_or_create_dm(
1161                bola.inbox_id().to_string(),
1162                Some(DMMetadataOptions::default()),
1163            )
1164            .await
1165            .unwrap();
1166
1167            // Sync both clients
1168            amal.sync_welcomes().await.unwrap();
1169            bola.sync_welcomes().await.unwrap();
1170
1171            // Get the DMs from the clients
1172            let query_args = GroupQueryArgs {
1173                conversation_type: Some(ConversationType::Dm),
1174                ..GroupQueryArgs::default()
1175            };
1176
1177            let amal_convos = amal.list_conversations(query_args.clone()).unwrap();
1178            let bola_convos = bola.list_conversations(query_args).unwrap();
1179
1180            assert_eq!(amal_convos.len(), 1);
1181            assert_eq!(bola_convos.len(), 1);
1182
1183            let amal_key_package = get_key_package_from_network(&amal).await;
1184            let bola_key_package = get_key_package_from_network(&bola).await;
1185
1186            assert_eq!(
1187                amal_key_package.wrapper_encryption().unwrap().is_some(),
1188                amal_has_pq
1189            );
1190            assert_eq!(
1191                bola_key_package.wrapper_encryption().unwrap().is_some(),
1192                bola_has_pq
1193            );
1194
1195            // Get the welcome messages from the network
1196            let bola_welcome = get_latest_welcome(&bola).await;
1197
1198            // Make sure the wrapper algorithms were set correctly in the Welcome messages
1199            let pq_algorithm = WrapperAlgorithm::XWingMLKEM768Draft6;
1200            let traditional_algorithm = WrapperAlgorithm::Curve25519;
1201            let bola_wrapper = bola_welcome.as_v1().unwrap().wrapper_algorithm;
1202            if bola_has_pq {
1203                assert_eq!(bola_wrapper, pq_algorithm.into());
1204            } else {
1205                assert_eq!(bola_wrapper, traditional_algorithm.into());
1206            }
1207        }
1208    }
1209}