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#[derive(Debug, Clone)]
66pub enum IdentityStrategy {
67 CreateIfNotFound {
69 inbox_id: InboxId,
70 identifier: Identifier,
71 nonce: u64,
72 legacy_signed_private_key: Option<Vec<u8>>,
73 },
74 CachedOnly,
76 #[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 #[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 #[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 #[error(transparent)]
182 CredentialSerialization(#[from] prost::EncodeError),
183 #[error(transparent)]
187 Decode(#[from] prost::DecodeError),
188 #[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 #[error(transparent)]
203 BasicCredential(#[from] BasicCredentialError),
204 #[error("Legacy key re-use")]
208 LegacyKeyReuse,
209 #[error("Uninitialized identity")]
213 UninitializedIdentity,
214 #[error("Installation key {0}")]
218 InstallationKey(String),
219 #[error("Malformed legacy key: {0}")]
223 MalformedLegacyKey(String),
224 #[error("Legacy signature: {0}")]
228 LegacySignature(String),
229 #[error(transparent)]
233 Crypto(#[from] CryptoError),
234 #[error("legacy key does not match address")]
238 LegacyKeyMismatch,
239 #[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 #[error(transparent)]
254 KeyPackageGenerationError(#[from] openmls::key_packages::errors::KeyPackageNewError),
255 #[error(transparent)]
256 #[error_code(inherit)]
257 KeyPackageVerificationError(#[from] KeyPackageVerificationError),
258 #[error("The InboxID {id}, associated does not match the stored InboxId {stored}.")]
262 InboxIdMismatch { id: InboxId, stored: InboxId },
263 #[error("The address {0} has no associated InboxID")]
267 NoAssociatedInboxId(String),
268 #[error("Required identity was not found in cache.")]
272 RequiredIdentityNotFound,
273 #[error("error creating new identity: {0}")]
277 NewIdentity(String),
278 #[error(transparent)]
279 #[error_code(inherit)]
280 Association(#[from] AssociationError),
281 #[error(transparent)]
285 Signer(#[from] xmtp_cryptography::SignerError),
286 #[error(transparent)]
287 #[error_code(inherit)]
288 ApiClient(#[from] xmtp_api::ApiError),
289 #[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 #[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 #[error(transparent)]
316 InvalidExtension(#[from] openmls::prelude::InvalidExtensionError),
317 #[error("Missing post quantum public key")]
321 MissingPostQuantumPublicKey,
322 #[error("Bincode serialization error")]
326 Bincode,
327 #[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 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 #[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 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 *associated_inbox_id != inbox_id {
447 return Err(IdentityError::NewIdentity("Inbox ID mismatch".to_string()));
448 }
449
450 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 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 if nonce != 0 {
516 return Err(IdentityError::NewIdentity(
517 "Nonce must be 0 if legacy key is provided".to_string(),
518 ));
519 }
520 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 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 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 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 #[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 #[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 let (kp_bytes, history_id) =
731 self.generate_and_store_key_package(mls_storage, include_post_quantum)?;
732
733 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 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 #[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 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
869pub(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
883pub(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
913pub(crate) fn store_key_package_references(
918 provider: &impl MlsProviderExt,
919 kp: &KeyPackage,
920 post_quantum_keypair: &Option<HpkeKeyPair>,
922) -> Result<(), IdentityError> {
923 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 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 let post_quantum_private_key = bincode::serialize(&post_quantum_keypair.private.to_vec())
937 .map_err(|_| IdentityError::Bincode)?;
938
939 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 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 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 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 let init_key_hash_ref = get_hash_ref(&provider, &starting_init_key);
1040 assert!(init_key_hash_ref.is_some());
1041
1042 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 let key_package_bundle: KeyPackageBundle = provider
1056 .storage()
1057 .key_package(&pq_hash_ref_inner)
1058 .unwrap()
1059 .unwrap();
1060
1061 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 client.rotate_and_upload_key_package().await.unwrap();
1068
1069 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 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 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 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 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 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 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 amal.find_or_create_dm(
1200 bola.inbox_id().to_string(),
1201 Some(DMMetadataOptions::default()),
1202 )
1203 .await
1204 .unwrap();
1205
1206 amal.sync_welcomes().await.unwrap();
1208 bola.sync_welcomes().await.unwrap();
1209
1210 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 let bola_welcome = get_latest_welcome(&bola).await;
1236
1237 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}