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