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#[derive(Debug, Clone)]
69pub enum IdentityStrategy {
70 CreateIfNotFound {
72 inbox_id: InboxId,
73 identifier: Identifier,
74 nonce: u64,
75 },
76 CreateForIdentifier { identifier: Identifier, nonce: u64 },
79 CachedOnly,
81 #[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 #[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 pub fn for_identifier(identifier: Identifier, nonce: u64) -> Self {
109 Self::CreateForIdentifier { identifier, nonce }
110 }
111
112 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 #[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 #[error(transparent)]
218 CredentialSerialization(#[from] prost::EncodeError),
219 #[error(transparent)]
223 Decode(#[from] prost::DecodeError),
224 #[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 #[error(transparent)]
239 BasicCredential(#[from] BasicCredentialError),
240 #[error("Uninitialized identity")]
244 UninitializedIdentity,
245 #[error("Installation key {0}")]
249 InstallationKey(String),
250 #[error(transparent)]
254 Crypto(#[from] CryptoError),
255 #[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 #[error(transparent)]
270 KeyPackageGenerationError(#[from] openmls::key_packages::errors::KeyPackageNewError),
271 #[error(transparent)]
272 #[error_code(inherit)]
273 KeyPackageVerificationError(#[from] KeyPackageVerificationError),
274 #[error("The InboxID {id}, associated does not match the stored InboxId {stored}.")]
278 InboxIdMismatch { id: InboxId, stored: InboxId },
279 #[error("The identifier does not belong to the stored InboxId {inbox_id}.")]
283 IdentifierNotInInbox { inbox_id: InboxId },
284 #[error("The address {0} has no associated InboxID")]
288 NoAssociatedInboxId(String),
289 #[error("Required identity was not found in cache.")]
293 RequiredIdentityNotFound,
294 #[error("error creating new identity: {0}")]
298 NewIdentity(String),
299 #[error(transparent)]
300 #[error_code(inherit)]
301 Association(#[from] AssociationError),
302 #[error(transparent)]
306 Signer(#[from] xmtp_cryptography::SignerError),
307 #[error(transparent)]
308 #[error_code(inherit)]
309 ApiClient(#[from] xmtp_api::ApiError),
310 #[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 #[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 #[error(transparent)]
337 InvalidExtension(#[from] openmls::prelude::InvalidExtensionError),
338 #[error("Missing post quantum public key")]
342 MissingPostQuantumPublicKey,
343 #[error("Bincode serialization error")]
347 Bincode,
348 #[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 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 #[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 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 *associated_inbox_id != inbox_id {
463 return Err(IdentityError::NewIdentity("Inbox ID mismatch".to_string()));
464 }
465
466 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 let max_installations = api_client.configuration().mls.max_installations_per_inbox;
491 let current_installation_count = state.installation_ids().len();
492 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 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 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 #[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 #[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 let (kp_bytes, history_id) =
655 self.generate_and_store_key_package(mls_storage, include_post_quantum)?;
656
657 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 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 #[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
782pub(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
796pub(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
826pub(crate) fn store_key_package_references(
831 provider: &impl MlsProviderExt,
832 kp: &KeyPackage,
833 post_quantum_keypair: &Option<HpkeKeyPair>,
835) -> Result<(), IdentityError> {
836 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 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 let post_quantum_private_key = bincode::serialize(&post_quantum_keypair.private.to_vec())
850 .map_err(|_| IdentityError::Bincode)?;
851
852 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 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 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 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 let init_key_hash_ref = get_hash_ref(&provider, &starting_init_key);
1007 assert!(init_key_hash_ref.is_some());
1008
1009 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 let key_package_bundle: KeyPackageBundle = provider
1023 .storage()
1024 .key_package(&pq_hash_ref_inner)
1025 .unwrap()
1026 .unwrap();
1027
1028 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 client.rotate_and_upload_key_package().await.unwrap();
1035
1036 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 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 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 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 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 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 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 amal.find_or_create_dm(
1161 bola.inbox_id().to_string(),
1162 Some(DMMetadataOptions::default()),
1163 )
1164 .await
1165 .unwrap();
1166
1167 amal.sync_welcomes().await.unwrap();
1169 bola.sync_welcomes().await.unwrap();
1170
1171 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 let bola_welcome = get_latest_welcome(&bola).await;
1197
1198 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}