1use tracing::info;
2
3pub(crate) use coven_core::keys::{public_key_hex, SIGN_PUBLICKEYBYTES, SIGN_SECRETKEYBYTES};
7pub use coven_core::keys::{
8 CloudHomeCredentials, DeviceIdentityCustody, KeyError, MasterKeyCustody, UserKeypair,
9};
10
11#[derive(Debug, thiserror::Error)]
14pub enum MasterKeyError {
15 #[error("a master key is already established for this store")]
19 AlreadyEstablished,
20 #[error("key error: {0}")]
21 Key(#[from] KeyError),
22 #[error("invalid master key material: {0}")]
23 Encryption(#[from] crate::encryption::EncryptionError),
24}
25
26#[derive(Debug, thiserror::Error)]
29pub enum IdentityError {
30 #[error("an identity is already established for this store")]
35 AlreadyEstablished,
36 #[error("key error: {0}")]
37 Key(#[from] KeyError),
38}
39
40static KEYRING_SERVICE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
41
42pub fn set_keyring_service(name: impl Into<String>) -> Result<(), KeyError> {
50 crate::keyring_backend::install_platform_store()?;
51 let name = name.into();
52 if KEYRING_SERVICE.set(name.clone()).is_err() {
53 let registered = KEYRING_SERVICE
54 .get()
55 .map(String::as_str)
56 .expect("a keyring service is registered when set() fails");
57 if registered != name {
58 return Err(KeyError::Persistence(format!(
59 "keyring service is already registered as {registered:?}; cannot re-register as {name:?}"
60 )));
61 }
62 }
63 Ok(())
64}
65
66pub fn keyring_service() -> Result<&'static str, KeyError> {
70 KEYRING_SERVICE
71 .get()
72 .map(String::as_str)
73 .ok_or(KeyError::ServiceNotRegistered)
74}
75
76fn map_keyring_error(e: keyring_core::Error) -> KeyError {
77 #[cfg(any(target_os = "macos", target_os = "ios"))]
78 if is_missing_keychain_entitlement(&e) {
79 return KeyError::MissingKeychainEntitlement;
80 }
81 match e {
82 keyring_core::Error::NoDefaultStore => KeyError::StoreNotInstalled,
83 other => KeyError::Persistence(other.to_string()),
84 }
85}
86
87#[cfg(any(target_os = "macos", target_os = "ios"))]
98fn is_missing_keychain_entitlement(e: &keyring_core::Error) -> bool {
99 let keyring_core::Error::PlatformFailure(inner) = e else {
100 return false;
101 };
102 inner
103 .downcast_ref::<security_framework::base::Error>()
104 .is_some_and(|err| err.code() == -34018)
105}
106
107const DEVICE_SIGNING_KEY_BASE: &str = "coven_user_signing_key";
110const ENCRYPTION_MASTER_KEY_BASE: &str = "encryption_master_key";
113const CLOUD_HOME_CREDENTIALS_BASE: &str = "cloud_home_credentials";
116const PENDING_IDENTITY_BASE: &str = "coven_pending_identity";
119
120pub(crate) const RESERVED_HOST_SECRET_NAMES: &[&str] = &[
126 DEVICE_SIGNING_KEY_BASE,
127 ENCRYPTION_MASTER_KEY_BASE,
128 CLOUD_HOME_CREDENTIALS_BASE,
129 PENDING_IDENTITY_BASE,
130];
131
132pub(crate) enum KeyringSlot {
141 DeviceSigningKey(String),
143 EncryptionMasterKey(String),
145 CloudHomeCredentials(String),
147 PendingIdentity(String),
150 HostSecret { name: String, store_id: String },
154}
155
156impl KeyringSlot {
157 pub(crate) fn account(&self) -> String {
164 match self {
165 KeyringSlot::DeviceSigningKey(store_id) => {
166 format!("{DEVICE_SIGNING_KEY_BASE}:{store_id}")
167 }
168 KeyringSlot::EncryptionMasterKey(store_id) => {
169 format!("{ENCRYPTION_MASTER_KEY_BASE}:{store_id}")
170 }
171 KeyringSlot::CloudHomeCredentials(store_id) => {
172 format!("{CLOUD_HOME_CREDENTIALS_BASE}:{store_id}")
173 }
174 KeyringSlot::PendingIdentity(request_public_key_hex) => {
175 format!("{PENDING_IDENTITY_BASE}:{request_public_key_hex}")
176 }
177 KeyringSlot::HostSecret { name, store_id } => format!("{name}:{store_id}"),
178 }
179 }
180}
181
182pub(crate) fn validate_host_secret_name(name: &str) -> Result<(), KeyError> {
188 if name.is_empty() {
189 return Err(KeyError::InvalidSecretName {
190 name: name.to_string(),
191 reason: "a host secret name must not be empty".to_string(),
192 });
193 }
194 if name.contains(':') {
195 return Err(KeyError::InvalidSecretName {
196 name: name.to_string(),
197 reason: "a host secret name must not contain ':', the keyring account scheme's \
198 separator"
199 .to_string(),
200 });
201 }
202 if RESERVED_HOST_SECRET_NAMES.contains(&name) {
203 return Err(KeyError::InvalidSecretName {
204 name: name.to_string(),
205 reason: "reserved for coven's own keyring entries".to_string(),
206 });
207 }
208 Ok(())
209}
210
211#[cfg(any(target_os = "macos", target_os = "ios"))]
245pub(crate) fn entry_for(account: &str) -> Result<keyring_core::Entry, KeyError> {
246 let service = keyring_service()?;
247 let store = keyring_core::get_default_store().ok_or(KeyError::StoreNotInstalled)?;
248 match store
249 .as_any()
250 .downcast_ref::<apple_native_keyring_store::protected::Store>()
251 {
252 Some(_) => apple_native_keyring_store::protected::Cred::build(
253 service,
254 account,
255 apple_native_keyring_store::protected::AccessPolicy::WhenUnlockedThisDeviceOnly,
256 None,
257 false,
258 )
259 .map_err(map_keyring_error),
260 None => keyring_core::Entry::new(service, account).map_err(map_keyring_error),
261 }
262}
263
264#[cfg(not(any(target_os = "macos", target_os = "ios")))]
265pub(crate) fn entry_for(account: &str) -> Result<keyring_core::Entry, KeyError> {
266 keyring_core::Entry::new(keyring_service()?, account).map_err(map_keyring_error)
267}
268
269#[cfg(any(test, feature = "test-utils"))]
274pub fn entry_for_test(account: &str) -> Result<keyring_core::Entry, KeyError> {
275 entry_for(account)
276}
277
278pub(crate) fn read(slot: &KeyringSlot) -> Result<Option<String>, KeyError> {
279 let account = slot.account();
280 let entry = entry_for(&account)?;
281 match entry.get_password() {
282 Ok(p) if p.is_empty() => Err(KeyError::Persistence(format!(
283 "keyring entry {account} is present but empty (corrupt)"
284 ))),
285 Ok(p) => Ok(Some(p)),
286 Err(keyring_core::Error::NoEntry) => Ok(None),
287 Err(e) => Err(map_keyring_error(e)),
288 }
289}
290
291pub(crate) fn write(slot: &KeyringSlot, value: &str) -> Result<(), KeyError> {
292 entry_for(&slot.account())?
293 .set_password(value)
294 .map_err(map_keyring_error)
295}
296
297pub(crate) fn delete(slot: &KeyringSlot) -> Result<bool, KeyError> {
298 match entry_for(&slot.account())?.delete_credential() {
299 Ok(()) => Ok(true),
300 Err(keyring_core::Error::NoEntry) => Ok(false),
301 Err(e) => Err(map_keyring_error(e)),
302 }
303}
304
305pub(crate) fn require_identity(
310 custody: &dyn DeviceIdentityCustody,
311) -> Result<UserKeypair, KeyError> {
312 custody.unlock()?.ok_or(KeyError::NoDeviceIdentity)
313}
314
315pub(crate) fn identity_public_key(
318 custody: &dyn DeviceIdentityCustody,
319) -> Result<Option<[u8; SIGN_PUBLICKEYBYTES]>, KeyError> {
320 Ok(custody.unlock()?.map(|kp| kp.public_key()))
321}
322
323pub(crate) fn import_identity(
329 custody: &dyn DeviceIdentityCustody,
330 signing_key_bytes: &[u8],
331) -> Result<(), KeyError> {
332 let signing_key: [u8; SIGN_SECRETKEYBYTES] = signing_key_bytes.try_into().map_err(|_| {
333 KeyError::Crypto(format!(
334 "Signing key must be {SIGN_SECRETKEYBYTES} bytes, got {}",
335 signing_key_bytes.len()
336 ))
337 })?;
338 let imported = UserKeypair::from_signing_key_bytes(&signing_key)?;
339
340 if let Some(existing) = custody.unlock()? {
341 if existing.public_key() != imported.public_key() {
342 return Err(KeyError::IdentityMismatch {
343 existing_pubkey_hex: public_key_hex(&existing),
344 imported_pubkey_hex: public_key_hex(&imported),
345 });
346 }
347 }
348 custody.persist(&imported)?;
349 info!("Imported this store's Ed25519 signing identity");
350 Ok(())
351}
352
353pub(crate) fn mint_pending_identity() -> Result<UserKeypair, KeyError> {
365 let keypair = UserKeypair::generate();
366 write(
367 &KeyringSlot::PendingIdentity(public_key_hex(&keypair)),
368 &hex::encode(keypair.to_keypair_bytes()),
369 )?;
370 info!("Minted a pending identity for a join request");
371 Ok(keypair)
372}
373
374pub(crate) fn peek_pending_identity(request_public_key_hex: &str) -> Result<UserKeypair, KeyError> {
380 read_pending_identity_slot(&KeyringSlot::PendingIdentity(
381 request_public_key_hex.to_string(),
382 ))
383}
384
385fn read_pending_identity_slot(slot: &KeyringSlot) -> Result<UserKeypair, KeyError> {
386 let KeyringSlot::PendingIdentity(request_public_key_hex) = slot else {
387 unreachable!("read_pending_identity_slot is only ever called with a PendingIdentity slot");
388 };
389 let sk_hex = read(slot)?.ok_or_else(|| KeyError::NoPendingIdentity {
390 request_public_key_hex: request_public_key_hex.clone(),
391 })?;
392 let signing_key: [u8; SIGN_SECRETKEYBYTES] = hex::decode(&sk_hex)
393 .map_err(|e| KeyError::Crypto(format!("invalid pending identity hex: {e}")))?
394 .try_into()
395 .map_err(|_| KeyError::Crypto("pending identity wrong length".to_string()))?;
396 UserKeypair::from_signing_key_bytes(&signing_key)
397}
398
399pub(crate) fn discard_pending_identity(request_public_key_hex: &str) -> Result<(), KeyError> {
404 delete(&KeyringSlot::PendingIdentity(
405 request_public_key_hex.to_string(),
406 ))
407 .map(|_| ())
408}
409
410#[derive(Clone)]
416pub struct StoreKeys {
417 store_id: String,
418}
419
420impl StoreKeys {
421 pub fn new(store_id: String) -> Self {
422 Self { store_id }
423 }
424
425 pub fn store_id(&self) -> &str {
426 &self.store_id
427 }
428
429 pub fn get_encryption_key(&self) -> Result<Option<String>, KeyError> {
430 read(&KeyringSlot::EncryptionMasterKey(self.store_id.clone()))
431 }
432
433 pub fn set_encryption_key(&self, value: &str) -> Result<(), KeyError> {
434 write(
435 &KeyringSlot::EncryptionMasterKey(self.store_id.clone()),
436 value,
437 )?;
438 info!("Encryption key saved to keyring");
439 Ok(())
440 }
441
442 pub fn delete_encryption_key(&self) -> Result<(), KeyError> {
443 if delete(&KeyringSlot::EncryptionMasterKey(self.store_id.clone()))? {
444 info!("Encryption key deleted from keyring");
445 }
446 Ok(())
447 }
448
449 pub fn get_cloud_home_credentials(&self) -> Result<Option<CloudHomeCredentials>, KeyError> {
450 match read(&KeyringSlot::CloudHomeCredentials(self.store_id.clone()))? {
451 None => Ok(None),
452 Some(j) => serde_json::from_str(&j).map(Some).map_err(|e| {
453 KeyError::Crypto(format!("malformed cloud home credentials JSON: {e}"))
454 }),
455 }
456 }
457
458 pub fn set_cloud_home_credentials(&self, creds: &CloudHomeCredentials) -> Result<(), KeyError> {
459 let json = serde_json::to_string(creds)
460 .map_err(|e| KeyError::Crypto(format!("serialize credentials: {e}")))?;
461 write(
462 &KeyringSlot::CloudHomeCredentials(self.store_id.clone()),
463 &json,
464 )?;
465 info!("Cloud home credentials saved to keyring");
466 Ok(())
467 }
468
469 #[cfg(feature = "oauth-providers")]
470 pub fn set_cloud_home_oauth_tokens(
471 &self,
472 tokens: &crate::oauth::OAuthTokens,
473 ) -> Result<(), KeyError> {
474 let token_json = serde_json::to_string(tokens)
475 .map_err(|e| KeyError::Crypto(format!("serialize OAuth tokens: {e}")))?;
476 self.set_cloud_home_credentials(&CloudHomeCredentials::OAuth { token_json })
477 }
478
479 #[cfg(test)]
480 pub(crate) fn cloud_home_credentials_entry_for_test(
481 &self,
482 ) -> Result<keyring_core::Entry, KeyError> {
483 entry_for(&KeyringSlot::CloudHomeCredentials(self.store_id.clone()).account())
484 }
485
486 pub fn delete_cloud_home_credentials(&self) -> Result<(), KeyError> {
487 if delete(&KeyringSlot::CloudHomeCredentials(self.store_id.clone()))? {
488 info!("Cloud home credentials deleted from keyring");
489 }
490 Ok(())
491 }
492
493 fn host_secret_slot(&self, name: &str) -> KeyringSlot {
494 KeyringSlot::HostSecret {
495 name: name.to_string(),
496 store_id: self.store_id.clone(),
497 }
498 }
499
500 pub fn get_host_secret(&self, name: &str) -> Result<Option<String>, KeyError> {
506 validate_host_secret_name(name)?;
507 read(&self.host_secret_slot(name))
508 }
509
510 pub fn set_host_secret(&self, name: &str, value: &str) -> Result<(), KeyError> {
513 validate_host_secret_name(name)?;
514 write(&self.host_secret_slot(name), value)?;
515 info!("Host secret {name:?} saved to keyring");
516 Ok(())
517 }
518
519 pub fn delete_host_secret(&self, name: &str) -> Result<(), KeyError> {
522 validate_host_secret_name(name)?;
523 if delete(&self.host_secret_slot(name))? {
524 info!("Host secret {name:?} deleted from keyring");
525 }
526 Ok(())
527 }
528}
529
530#[cfg(test)]
531pub(crate) mod test_keyring {
532 use std::sync::Once;
533
534 static INSTALL: Once = Once::new();
535
536 pub(crate) fn install() {
537 INSTALL.call_once(|| {
538 keyring_core::set_default_store(
542 keyring_core::mock::Store::new().expect("create mock keyring store"),
543 );
544 super::set_keyring_service("coven-tests").expect("register keyring service");
545 });
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[cfg(any(target_os = "macos", target_os = "ios"))]
565 #[test]
566 fn missing_entitlement_os_status_maps_to_a_typed_actionable_error() {
567 let raw = keyring_core::Error::PlatformFailure(Box::new(
568 security_framework::base::Error::from_code(-34018),
569 ));
570
571 let mapped = map_keyring_error(raw);
572
573 assert!(
574 matches!(mapped, KeyError::MissingKeychainEntitlement),
575 "got {mapped:?}"
576 );
577 let message = mapped.to_string();
578 assert!(message.contains("-34018"), "{message}");
579 assert!(message.contains("errSecMissingEntitlement"), "{message}");
580 assert!(message.contains("keychain-access-groups"), "{message}");
581 assert!(message.contains("provisioning profile"), "{message}");
582 assert!(message.contains("DEVELOPMENT_TEAM"), "{message}");
583 assert!(
584 !message.contains("must be signed"),
585 "a bare 'signed binary' is not the fix and must not be implied: {message}"
586 );
587 }
588
589 #[cfg(any(target_os = "macos", target_os = "ios"))]
594 #[test]
595 fn a_different_platform_failure_os_status_is_not_reported_as_missing_entitlement() {
596 let raw = keyring_core::Error::PlatformFailure(Box::new(
597 security_framework::base::Error::from_code(-25291), ));
599
600 let mapped = map_keyring_error(raw);
601
602 assert!(matches!(mapped, KeyError::Persistence(_)), "got {mapped:?}");
603 }
604
605 #[test]
606 fn empty_keyring_entry_is_an_error_not_absence() {
607 test_keyring::install();
608 let slot = KeyringSlot::EncryptionMasterKey("empty-keyring-entry-store".to_string());
609 let account = slot.account();
610 entry_for(&account)
611 .expect("create keyring entry")
612 .set_password("")
613 .expect("write empty keyring entry");
614
615 let error = read(&slot).expect_err("empty entry is corrupt");
616
617 assert!(error.to_string().contains("present but empty"));
618 assert!(error.to_string().contains(&account));
619 }
620
621 #[test]
630 fn keyring_account_names_are_a_stable_storage_contract() {
631 assert_eq!(
632 KeyringSlot::EncryptionMasterKey("store-42".to_string()).account(),
633 "encryption_master_key:store-42"
634 );
635 assert_eq!(
636 KeyringSlot::CloudHomeCredentials("store-42".to_string()).account(),
637 "cloud_home_credentials:store-42"
638 );
639 assert_eq!(
640 KeyringSlot::DeviceSigningKey("store-42".to_string()).account(),
641 "coven_user_signing_key:store-42"
642 );
643 assert_eq!(
644 KeyringSlot::PendingIdentity("deadbeef".to_string()).account(),
645 "coven_pending_identity:deadbeef"
646 );
647 assert_eq!(
648 KeyringSlot::HostSecret {
649 name: "discogs_api_key".to_string(),
650 store_id: "s1".to_string(),
651 }
652 .account(),
653 "discogs_api_key:s1"
654 );
655 }
656
657 #[test]
662 fn host_secret_round_trips_and_absent_reads_none() {
663 test_keyring::install();
664 let keys = StoreKeys::new("host-secret-round-trip".to_string());
665
666 assert_eq!(
667 keys.get_host_secret("discogs_api_key").expect("get"),
668 None,
669 "an unset host secret reads as absent",
670 );
671
672 keys.set_host_secret("discogs_api_key", "the-discogs-key")
673 .expect("set");
674 assert_eq!(
675 keys.get_host_secret("discogs_api_key").expect("get"),
676 Some("the-discogs-key".to_string()),
677 );
678
679 keys.delete_host_secret("discogs_api_key").expect("delete");
680 assert_eq!(
681 keys.get_host_secret("discogs_api_key")
682 .expect("get after delete"),
683 None,
684 );
685 }
686
687 #[test]
691 fn host_secret_refuses_every_reserved_name() {
692 test_keyring::install();
693 let keys = StoreKeys::new("host-secret-reserved-names".to_string());
694
695 for reserved in RESERVED_HOST_SECRET_NAMES {
696 let error = keys.set_host_secret(reserved, "value").expect_err(&format!(
697 "{reserved:?} must be refused as a host secret name"
698 ));
699 assert!(
700 matches!(error, KeyError::InvalidSecretName { .. }),
701 "got {error:?}",
702 );
703 }
704 }
705
706 #[test]
707 fn host_secret_refuses_a_name_containing_colon() {
708 test_keyring::install();
709 let keys = StoreKeys::new("host-secret-colon-name".to_string());
710
711 let error = keys
712 .set_host_secret("discogs:api_key", "value")
713 .expect_err("a name containing ':' must be refused");
714 assert!(
715 matches!(error, KeyError::InvalidSecretName { .. }),
716 "{error:?}"
717 );
718 }
719
720 #[test]
721 fn host_secret_refuses_an_empty_name() {
722 test_keyring::install();
723 let keys = StoreKeys::new("host-secret-empty-name".to_string());
724
725 let error = keys
726 .set_host_secret("", "value")
727 .expect_err("an empty name must be refused");
728 assert!(
729 matches!(error, KeyError::InvalidSecretName { .. }),
730 "{error:?}"
731 );
732 }
733
734 #[test]
738 fn host_secret_present_but_empty_is_an_error_not_absence() {
739 test_keyring::install();
740 let slot = KeyringSlot::HostSecret {
741 name: "discogs_api_key".to_string(),
742 store_id: "host-secret-empty-entry-store".to_string(),
743 };
744 let account = slot.account();
745 entry_for(&account)
746 .expect("create keyring entry")
747 .set_password("")
748 .expect("write empty keyring entry");
749
750 let keys = StoreKeys::new("host-secret-empty-entry-store".to_string());
751 let error = keys
752 .get_host_secret("discogs_api_key")
753 .expect_err("empty entry is corrupt");
754 assert!(error.to_string().contains("present but empty"));
755 }
756
757 #[test]
760 fn host_secret_is_scoped_to_its_store() {
761 test_keyring::install();
762 let store_a = StoreKeys::new("host-secret-scope-a".to_string());
763 let store_b = StoreKeys::new("host-secret-scope-b".to_string());
764
765 store_a
766 .set_host_secret("discogs_api_key", "key-for-store-a")
767 .expect("set on store a");
768
769 assert_eq!(
770 store_a.get_host_secret("discogs_api_key").expect("get"),
771 Some("key-for-store-a".to_string()),
772 );
773 assert_eq!(
774 store_b.get_host_secret("discogs_api_key").expect("get"),
775 None,
776 "store b must not see store a's secret",
777 );
778 }
779
780 fn test_identity_custody(store_id: &str) -> std::sync::Arc<dyn DeviceIdentityCustody> {
783 crate::identity_custody::IdentityCustody::Keyring.resolve(
784 store_id,
785 &crate::store_dir::StoreDir::new("unused-store-dir"),
786 )
787 }
788
789 #[test]
794 fn require_identity_reads_a_keypair_written_at_the_stores_account() {
795 test_keyring::install();
796 let store_id = "require-identity-fixed-account-test";
797
798 let keypair = UserKeypair::generate();
799 let expected_pubkey = keypair.public_key();
800 write(
804 &KeyringSlot::DeviceSigningKey(store_id.to_string()),
805 &hex::encode(keypair.to_keypair_bytes()),
806 )
807 .expect("write signing key to the raw keyring");
808
809 let custody = test_identity_custody(store_id);
810 let read = require_identity(custody.as_ref()).expect("read the identity back");
811 assert_eq!(
812 read.public_key(),
813 expected_pubkey,
814 "require_identity must read the keypair stored at the store's account",
815 );
816 }
817
818 #[test]
822 fn require_identity_maps_absence_to_no_device_identity() {
823 test_keyring::install();
824 let custody = test_identity_custody("require-identity-absent-test");
825
826 match require_identity(custody.as_ref()) {
827 Err(error) => assert!(matches!(error, KeyError::NoDeviceIdentity), "got {error:?}"),
828 Ok(_) => panic!("no identity is established"),
829 }
830 }
831
832 #[test]
837 fn import_identity_same_pubkey_reimport_is_idempotent() {
838 test_keyring::install();
839 let custody = test_identity_custody("import-identity-idempotent-test");
840
841 let keypair = UserKeypair::generate();
842 import_identity(custody.as_ref(), &keypair.to_keypair_bytes())
843 .expect("first import establishes the identity");
844 import_identity(custody.as_ref(), &keypair.to_keypair_bytes())
845 .expect("re-importing the same key is idempotent");
846
847 assert_eq!(
848 require_identity(custody.as_ref())
849 .expect("identity still readable")
850 .public_key(),
851 keypair.public_key(),
852 );
853 }
854
855 #[test]
859 fn import_identity_refuses_to_overwrite_a_different_identity() {
860 test_keyring::install();
861 let custody = test_identity_custody("import-identity-mismatch-test");
862
863 let established = UserKeypair::generate();
864 import_identity(custody.as_ref(), &established.to_keypair_bytes())
865 .expect("establish the first identity");
866
867 let different = UserKeypair::generate();
868 let error = import_identity(custody.as_ref(), &different.to_keypair_bytes())
869 .expect_err("importing a different identity must be refused");
870 match error {
871 KeyError::IdentityMismatch {
872 existing_pubkey_hex,
873 imported_pubkey_hex,
874 } => {
875 assert_eq!(existing_pubkey_hex, public_key_hex(&established));
876 assert_eq!(imported_pubkey_hex, public_key_hex(&different));
877 }
878 other => panic!("expected IdentityMismatch, got {other:?}"),
879 }
880
881 assert_eq!(
883 require_identity(custody.as_ref())
884 .expect("the original identity is untouched")
885 .public_key(),
886 established.public_key(),
887 );
888 }
889
890 #[test]
897 fn pending_identity_establishes_then_discards() {
898 test_keyring::install();
899 let pending = mint_pending_identity().expect("mint pending identity");
900 let request_pubkey = public_key_hex(&pending);
901 let custody = test_identity_custody("pending-identity-establish-test");
902
903 import_identity(custody.as_ref(), &pending.to_keypair_bytes())
904 .expect("establish the pending identity in store custody");
905 assert_eq!(
906 require_identity(custody.as_ref())
907 .expect("the store now has an identity")
908 .public_key(),
909 pending.public_key(),
910 );
911
912 let still_pending =
915 peek_pending_identity(&request_pubkey).expect("the pending slot is not yet consumed");
916 import_identity(custody.as_ref(), &still_pending.to_keypair_bytes())
917 .expect("re-establishing the same identity is idempotent");
918
919 discard_pending_identity(&request_pubkey).expect("discard the consumed slot");
920 let error = peek_pending_identity(&request_pubkey)
921 .map(|_| ())
922 .expect_err("the discarded slot no longer serves the identity");
923 assert!(
924 matches!(error, KeyError::NoPendingIdentity { .. }),
925 "{error:?}"
926 );
927 assert_eq!(
928 require_identity(custody.as_ref())
929 .expect("the established identity outlives the slot")
930 .public_key(),
931 pending.public_key(),
932 );
933 }
934
935 #[test]
938 fn discard_pending_identity_removes_it_and_is_idempotent() {
939 test_keyring::install();
940 let pending = mint_pending_identity().expect("mint pending identity");
941 let request_pubkey = public_key_hex(&pending);
942
943 discard_pending_identity(&request_pubkey).expect("discard the pending identity");
944 discard_pending_identity(&request_pubkey)
945 .expect("discarding an already-absent pending identity is not an error");
946
947 let error = peek_pending_identity(&request_pubkey)
948 .map(|_| ())
949 .expect_err("a discarded pending identity is no longer served");
950 assert!(
951 matches!(error, KeyError::NoPendingIdentity { .. }),
952 "{error:?}"
953 );
954 }
955
956 #[test]
959 fn two_concurrent_pending_joins_do_not_cross() {
960 test_keyring::install();
961 let pending_a = mint_pending_identity().expect("mint pending identity a");
962 let pending_b = mint_pending_identity().expect("mint pending identity b");
963 assert_ne!(pending_a.public_key(), pending_b.public_key());
964
965 let custody_a = test_identity_custody("two-concurrent-joins-store-a");
966 let custody_b = test_identity_custody("two-concurrent-joins-store-b");
967 import_identity(custody_a.as_ref(), &pending_a.to_keypair_bytes())
968 .expect("establish a into store a");
969
970 assert!(
971 require_identity(custody_b.as_ref()).is_err(),
972 "store b must not see store a's established identity",
973 );
974 import_identity(custody_b.as_ref(), &pending_b.to_keypair_bytes())
975 .expect("establish b into store b");
976 assert_ne!(
977 require_identity(custody_a.as_ref())
978 .expect("store a's identity")
979 .public_key(),
980 require_identity(custody_b.as_ref())
981 .expect("store b's identity")
982 .public_key(),
983 );
984 }
985}