@wlfi-agent/cli 1.4.17 → 1.4.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/Cargo.lock +5 -0
  2. package/README.md +61 -28
  3. package/crates/vault-cli-admin/src/io_utils.rs +149 -1
  4. package/crates/vault-cli-admin/src/main.rs +639 -16
  5. package/crates/vault-cli-admin/src/shared_config.rs +18 -18
  6. package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
  7. package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
  8. package/crates/vault-cli-admin/src/tui.rs +1205 -120
  9. package/crates/vault-cli-agent/Cargo.toml +1 -0
  10. package/crates/vault-cli-agent/src/io_utils.rs +163 -2
  11. package/crates/vault-cli-agent/src/main.rs +648 -32
  12. package/crates/vault-cli-daemon/Cargo.toml +4 -0
  13. package/crates/vault-cli-daemon/src/main.rs +617 -67
  14. package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
  15. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
  16. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
  17. package/crates/vault-daemon/src/persistence.rs +637 -100
  18. package/crates/vault-daemon/src/tests.rs +1013 -3
  19. package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
  20. package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
  21. package/crates/vault-domain/src/nonce.rs +4 -0
  22. package/crates/vault-domain/src/tests.rs +616 -0
  23. package/crates/vault-policy/src/engine.rs +55 -32
  24. package/crates/vault-policy/src/tests.rs +195 -0
  25. package/crates/vault-sdk-agent/src/lib.rs +415 -22
  26. package/crates/vault-signer/Cargo.toml +3 -0
  27. package/crates/vault-signer/src/lib.rs +266 -40
  28. package/crates/vault-transport-unix/src/lib.rs +653 -5
  29. package/crates/vault-transport-xpc/src/tests.rs +531 -3
  30. package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
  31. package/dist/cli.cjs +756 -194
  32. package/dist/cli.cjs.map +1 -1
  33. package/package.json +5 -2
  34. package/packages/cache/.turbo/turbo-build.log +20 -20
  35. package/packages/cache/coverage/clover.xml +529 -394
  36. package/packages/cache/coverage/coverage-final.json +2 -2
  37. package/packages/cache/coverage/index.html +21 -21
  38. package/packages/cache/coverage/src/client/index.html +1 -1
  39. package/packages/cache/coverage/src/client/index.ts.html +1 -1
  40. package/packages/cache/coverage/src/errors/index.html +1 -1
  41. package/packages/cache/coverage/src/errors/index.ts.html +12 -12
  42. package/packages/cache/coverage/src/index.html +1 -1
  43. package/packages/cache/coverage/src/index.ts.html +1 -1
  44. package/packages/cache/coverage/src/service/index.html +21 -21
  45. package/packages/cache/coverage/src/service/index.ts.html +769 -313
  46. package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
  47. package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
  48. package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
  49. package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
  50. package/packages/cache/dist/index.cjs +2 -2
  51. package/packages/cache/dist/index.js +1 -1
  52. package/packages/cache/dist/service/index.cjs +2 -2
  53. package/packages/cache/dist/service/index.js +1 -1
  54. package/packages/cache/node_modules/.bin/tsc +2 -2
  55. package/packages/cache/node_modules/.bin/tsserver +2 -2
  56. package/packages/cache/node_modules/.bin/tsup +2 -2
  57. package/packages/cache/node_modules/.bin/tsup-node +2 -2
  58. package/packages/cache/node_modules/.bin/vitest +4 -4
  59. package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  60. package/packages/cache/src/service/index.test.ts +165 -19
  61. package/packages/cache/src/service/index.ts +38 -1
  62. package/packages/config/.turbo/turbo-build.log +4 -4
  63. package/packages/config/dist/index.cjs +0 -17
  64. package/packages/config/dist/index.cjs.map +1 -1
  65. package/packages/config/src/index.ts +0 -17
  66. package/packages/rpc/.turbo/turbo-build.log +11 -11
  67. package/packages/rpc/dist/index.cjs +0 -17
  68. package/packages/rpc/dist/index.cjs.map +1 -1
  69. package/packages/rpc/src/index.js +1 -0
  70. package/packages/ui/node_modules/.bin/tsc +2 -2
  71. package/packages/ui/node_modules/.bin/tsserver +2 -2
  72. package/packages/ui/node_modules/.bin/tsup +2 -2
  73. package/packages/ui/node_modules/.bin/tsup-node +2 -2
  74. package/scripts/install-cli-launcher.mjs +37 -0
  75. package/scripts/install-rust-binaries.mjs +47 -0
  76. package/scripts/run-tests-isolated.mjs +210 -0
  77. package/src/cli.ts +310 -50
  78. package/src/lib/admin-reset.ts +101 -33
  79. package/src/lib/admin-setup.ts +285 -55
  80. package/src/lib/agent-auth-migrate.ts +5 -1
  81. package/src/lib/asset-broadcast.ts +15 -4
  82. package/src/lib/config-amounts.ts +6 -4
  83. package/src/lib/hidden-tty-prompt.js +1 -0
  84. package/src/lib/hidden-tty-prompt.ts +105 -0
  85. package/src/lib/keychain.ts +1 -0
  86. package/src/lib/local-admin-access.ts +4 -29
  87. package/src/lib/rust.ts +129 -33
  88. package/src/lib/signed-tx.ts +1 -0
  89. package/src/lib/sudo.ts +15 -5
  90. package/src/lib/wallet-profile.ts +3 -0
  91. package/src/lib/wallet-setup.ts +52 -0
  92. package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
  93. package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
@@ -17,21 +17,21 @@ use time::OffsetDateTime;
17
17
  use uuid::Uuid;
18
18
  use vault_domain::{KeySource, Signature, VaultKey};
19
19
 
20
- #[cfg(target_os = "macos")]
20
+ #[cfg(all(target_os = "macos", not(coverage)))]
21
21
  use core_foundation::base::{TCFType, ToVoid};
22
- #[cfg(target_os = "macos")]
22
+ #[cfg(all(target_os = "macos", not(coverage)))]
23
23
  use core_foundation::string::CFString;
24
- #[cfg(target_os = "macos")]
24
+ #[cfg(all(target_os = "macos", not(coverage)))]
25
25
  use security_framework::access_control::{ProtectionMode, SecAccessControl};
26
- #[cfg(target_os = "macos")]
26
+ #[cfg(all(target_os = "macos", not(coverage)))]
27
27
  use security_framework::item::{
28
28
  ItemClass, ItemSearchOptions, KeyClass, Limit, Location, Reference, SearchResult,
29
29
  };
30
- #[cfg(target_os = "macos")]
30
+ #[cfg(all(target_os = "macos", not(coverage)))]
31
31
  use security_framework::key::{Algorithm, GenerateKeyOptions, KeyType, SecKey, Token};
32
- #[cfg(target_os = "macos")]
32
+ #[cfg(all(target_os = "macos", not(coverage)))]
33
33
  use security_framework_sys::access_control::kSecAccessControlPrivateKeyUsage;
34
- #[cfg(target_os = "macos")]
34
+ #[cfg(all(target_os = "macos", not(coverage)))]
35
35
  use security_framework_sys::item::{
36
36
  kSecAttrAccessControl, kSecAttrTokenID, kSecAttrTokenIDSecureEnclave,
37
37
  };
@@ -227,7 +227,7 @@ impl VaultSignerBackend for SoftwareSignerBackend {
227
227
 
228
228
  let (signature, _) = signing_key
229
229
  .sign_prehash_recoverable(&digest)
230
- .map_err(|err| SignerError::Internal(format!("digest signature failed: {err}")))?;
230
+ .expect("32-byte digest and valid signing key must produce a recoverable signature");
231
231
  Ok(Signature::from_der(signature.to_der().as_bytes().to_vec()))
232
232
  }
233
233
 
@@ -294,9 +294,9 @@ impl SecureEnclaveSignerBackend {
294
294
  format!("{}.{key_id}", self.label_prefix)
295
295
  }
296
296
 
297
- #[cfg(target_os = "macos")]
298
- fn require_root() -> Result<(), SignerError> {
299
- if unsafe { libc::geteuid() } != 0 {
297
+ #[cfg(all(target_os = "macos", not(coverage)))]
298
+ fn require_root_euid(euid: u32) -> Result<(), SignerError> {
299
+ if euid != 0 {
300
300
  return Err(SignerError::PermissionDenied(
301
301
  "secure enclave backend requires root daemon context".to_string(),
302
302
  ));
@@ -304,7 +304,12 @@ impl SecureEnclaveSignerBackend {
304
304
  Ok(())
305
305
  }
306
306
 
307
- #[cfg(target_os = "macos")]
307
+ #[cfg(all(target_os = "macos", not(coverage)))]
308
+ fn require_root() -> Result<(), SignerError> {
309
+ Self::require_root_euid(unsafe { libc::geteuid() })
310
+ }
311
+
312
+ #[cfg(all(target_os = "macos", not(coverage)))]
308
313
  fn make_access_control() -> Result<SecAccessControl, SignerError> {
309
314
  SecAccessControl::create_with_protection(
310
315
  Some(ProtectionMode::AccessibleAfterFirstUnlockThisDeviceOnly),
@@ -313,7 +318,7 @@ impl SecureEnclaveSignerBackend {
313
318
  .map_err(|err| SignerError::Internal(format!("unable to create access control: {err}")))
314
319
  }
315
320
 
316
- #[cfg(target_os = "macos")]
321
+ #[cfg(all(target_os = "macos", not(coverage)))]
317
322
  fn generate_secure_enclave_key(&self, key_id: Uuid) -> Result<SecKey, SignerError> {
318
323
  let label = self.key_label(key_id);
319
324
  let mut options = GenerateKeyOptions::default();
@@ -330,7 +335,7 @@ impl SecureEnclaveSignerBackend {
330
335
  })
331
336
  }
332
337
 
333
- #[cfg(target_os = "macos")]
338
+ #[cfg(all(target_os = "macos", not(coverage)))]
334
339
  fn find_private_key(&self, key_id: Uuid) -> Result<SecKey, SignerError> {
335
340
  let label = self.key_label(key_id);
336
341
  let mut search = ItemSearchOptions::new();
@@ -369,7 +374,7 @@ impl SecureEnclaveSignerBackend {
369
374
  }
370
375
  }
371
376
 
372
- #[cfg(target_os = "macos")]
377
+ #[cfg(all(target_os = "macos", not(coverage)))]
373
378
  fn validate_secure_enclave_key_attributes(
374
379
  key: &SecKey,
375
380
  key_id: Uuid,
@@ -410,7 +415,7 @@ impl SecureEnclaveSignerBackend {
410
415
  Ok(())
411
416
  }
412
417
 
413
- #[cfg(target_os = "macos")]
418
+ #[cfg(all(target_os = "macos", not(coverage)))]
414
419
  fn public_key_hex(private_key: &SecKey) -> Result<String, SignerError> {
415
420
  let public_key = private_key
416
421
  .public_key()
@@ -421,7 +426,7 @@ impl SecureEnclaveSignerBackend {
421
426
  Ok(hex::encode(data.bytes()))
422
427
  }
423
428
 
424
- #[cfg(all(test, target_os = "macos"))]
429
+ #[cfg(all(test, target_os = "macos", feature = "interactive-secure-enclave-tests"))]
425
430
  fn delete_if_present(&self, key_id: Uuid) -> Result<(), SignerError> {
426
431
  match self.find_private_key(key_id) {
427
432
  Ok(key) => key
@@ -440,7 +445,7 @@ impl VaultSignerBackend for SecureEnclaveSignerBackend {
440
445
  }
441
446
 
442
447
  async fn create_vault_key(&self, request: KeyCreateRequest) -> Result<VaultKey, SignerError> {
443
- #[cfg(not(target_os = "macos"))]
448
+ #[cfg(any(not(target_os = "macos"), coverage))]
444
449
  {
445
450
  let _ = request;
446
451
  return Err(SignerError::Unsupported(
@@ -448,7 +453,7 @@ impl VaultSignerBackend for SecureEnclaveSignerBackend {
448
453
  ));
449
454
  }
450
455
 
451
- #[cfg(target_os = "macos")]
456
+ #[cfg(all(target_os = "macos", not(coverage)))]
452
457
  {
453
458
  match request {
454
459
  KeyCreateRequest::Generate => {
@@ -476,7 +481,7 @@ impl VaultSignerBackend for SecureEnclaveSignerBackend {
476
481
  vault_key_id: Uuid,
477
482
  payload: &[u8],
478
483
  ) -> Result<Signature, SignerError> {
479
- #[cfg(not(target_os = "macos"))]
484
+ #[cfg(any(not(target_os = "macos"), coverage))]
480
485
  {
481
486
  let _ = (vault_key_id, payload);
482
487
  return Err(SignerError::Unsupported(
@@ -484,7 +489,7 @@ impl VaultSignerBackend for SecureEnclaveSignerBackend {
484
489
  ));
485
490
  }
486
491
 
487
- #[cfg(target_os = "macos")]
492
+ #[cfg(all(target_os = "macos", not(coverage)))]
488
493
  {
489
494
  Self::require_root()?;
490
495
  let private_key = self.find_private_key(vault_key_id)?;
@@ -502,7 +507,7 @@ impl VaultSignerBackend for SecureEnclaveSignerBackend {
502
507
  vault_key_id: Uuid,
503
508
  digest: [u8; 32],
504
509
  ) -> Result<Signature, SignerError> {
505
- #[cfg(not(target_os = "macos"))]
510
+ #[cfg(any(not(target_os = "macos"), coverage))]
506
511
  {
507
512
  let _ = (vault_key_id, digest);
508
513
  return Err(SignerError::Unsupported(
@@ -510,7 +515,7 @@ impl VaultSignerBackend for SecureEnclaveSignerBackend {
510
515
  ));
511
516
  }
512
517
 
513
- #[cfg(target_os = "macos")]
518
+ #[cfg(all(target_os = "macos", not(coverage)))]
514
519
  {
515
520
  Self::require_root()?;
516
521
  let private_key = self.find_private_key(vault_key_id)?;
@@ -526,7 +531,177 @@ impl VaultSignerBackend for SecureEnclaveSignerBackend {
526
531
 
527
532
  #[cfg(test)]
528
533
  mod tests {
529
- use super::{KeyCreateRequest, SignerError, SoftwareSignerBackend, VaultSignerBackend};
534
+ use std::collections::HashMap;
535
+
536
+ use async_trait::async_trait;
537
+ use uuid::Uuid;
538
+
539
+ use super::{
540
+ AttestableSignerBackend, BackendKind, KeyCreateRequest, SecureEnclaveSignerBackend,
541
+ Signature, SignerError, SoftwareSignerBackend, VaultKey, VaultSignerBackend,
542
+ };
543
+
544
+ #[derive(Default)]
545
+ struct DummyTeeBackend;
546
+
547
+ #[async_trait]
548
+ impl VaultSignerBackend for DummyTeeBackend {
549
+ fn backend_kind(&self) -> BackendKind {
550
+ BackendKind::Tee
551
+ }
552
+
553
+ async fn create_vault_key(
554
+ &self,
555
+ _request: KeyCreateRequest,
556
+ ) -> Result<VaultKey, SignerError> {
557
+ Err(SignerError::Unsupported("not implemented".to_string()))
558
+ }
559
+
560
+ async fn sign_payload(
561
+ &self,
562
+ _vault_key_id: Uuid,
563
+ _payload: &[u8],
564
+ ) -> Result<Signature, SignerError> {
565
+ Err(SignerError::Unsupported("not implemented".to_string()))
566
+ }
567
+
568
+ async fn sign_digest(
569
+ &self,
570
+ _vault_key_id: Uuid,
571
+ _digest: [u8; 32],
572
+ ) -> Result<Signature, SignerError> {
573
+ Err(SignerError::Unsupported("not implemented".to_string()))
574
+ }
575
+ }
576
+
577
+ #[async_trait]
578
+ impl AttestableSignerBackend for DummyTeeBackend {
579
+ async fn attestation_document(&self) -> Result<Vec<u8>, SignerError> {
580
+ Ok(vec![0xde, 0xad, 0xbe, 0xef])
581
+ }
582
+ }
583
+
584
+ fn poison_backend_lock(backend: &SoftwareSignerBackend) {
585
+ let clone = backend.clone();
586
+ let _ = std::thread::spawn(move || {
587
+ let _guard = clone.keys.write().expect("write lock");
588
+ panic!("poison signer backend lock");
589
+ })
590
+ .join();
591
+ }
592
+
593
+ #[tokio::test]
594
+ async fn trait_defaults_and_backend_kinds_cover_remaining_variants() {
595
+ let backend = DummyTeeBackend;
596
+ assert_eq!(backend.backend_kind(), BackendKind::Tee);
597
+ assert!(matches!(
598
+ backend.create_vault_key(KeyCreateRequest::Generate).await,
599
+ Err(SignerError::Unsupported(message)) if message == "not implemented"
600
+ ));
601
+ assert!(matches!(
602
+ backend.sign_payload(Uuid::new_v4(), b"payload").await,
603
+ Err(SignerError::Unsupported(message)) if message == "not implemented"
604
+ ));
605
+ assert!(matches!(
606
+ backend.sign_digest(Uuid::new_v4(), [0x11; 32]).await,
607
+ Err(SignerError::Unsupported(message)) if message == "not implemented"
608
+ ));
609
+ assert_eq!(
610
+ backend
611
+ .export_persistable_key_material(&[])
612
+ .expect("default export"),
613
+ HashMap::new()
614
+ );
615
+ assert!(backend
616
+ .restore_persistable_key_material(&HashMap::new())
617
+ .is_ok());
618
+ assert!(matches!(
619
+ backend.restore_persistable_key_material(&HashMap::from([(
620
+ Uuid::new_v4(),
621
+ "11".repeat(32)
622
+ )])),
623
+ Err(SignerError::Unsupported(_))
624
+ ));
625
+ assert_eq!(
626
+ backend.attestation_document().await.expect("attestation"),
627
+ vec![0xde, 0xad, 0xbe, 0xef]
628
+ );
629
+
630
+ let software = SoftwareSignerBackend::default();
631
+ assert_eq!(software.backend_kind(), BackendKind::Software);
632
+
633
+ let enclave = SecureEnclaveSignerBackend::new("com.wlfi.coverage");
634
+ assert_eq!(enclave.backend_kind(), BackendKind::SecureEnclave);
635
+ assert_eq!(
636
+ enclave.key_label(Uuid::nil()),
637
+ "com.wlfi.coverage.00000000-0000-0000-0000-000000000000"
638
+ );
639
+ }
640
+
641
+ #[tokio::test]
642
+ async fn import_path_marks_keys_as_imported_and_accepts_prefixed_hex() {
643
+ let backend = SoftwareSignerBackend::default();
644
+ let key = backend
645
+ .create_vault_key(KeyCreateRequest::Import {
646
+ private_key_hex: format!("0x{}", "11".repeat(32)),
647
+ })
648
+ .await
649
+ .expect("must import key");
650
+
651
+ assert_eq!(key.source, vault_domain::KeySource::Imported);
652
+ assert!(!key.public_key_hex.is_empty());
653
+ }
654
+
655
+ #[tokio::test]
656
+ async fn software_backend_rejects_unknown_keys_and_poisoned_locks() {
657
+ let backend = SoftwareSignerBackend::default();
658
+ let unknown = Uuid::new_v4();
659
+ assert!(matches!(
660
+ backend.sign_payload(unknown, b"payload").await,
661
+ Err(SignerError::UnknownKey(id)) if id == unknown
662
+ ));
663
+ assert!(matches!(
664
+ backend.sign_digest(unknown, [0x11; 32]).await,
665
+ Err(SignerError::UnknownKey(id)) if id == unknown
666
+ ));
667
+ assert!(matches!(
668
+ backend.export_persistable_key_material(&[unknown]),
669
+ Err(SignerError::UnknownKey(id)) if id == unknown
670
+ ));
671
+
672
+ let poisoned = SoftwareSignerBackend::default();
673
+ poison_backend_lock(&poisoned);
674
+ assert!(matches!(
675
+ poisoned.create_vault_key(KeyCreateRequest::Generate).await,
676
+ Err(SignerError::Internal(_))
677
+ ));
678
+ assert!(matches!(
679
+ poisoned.sign_payload(Uuid::new_v4(), b"payload").await,
680
+ Err(SignerError::Internal(_))
681
+ ));
682
+ assert!(matches!(
683
+ poisoned.sign_digest(Uuid::new_v4(), [0x22; 32]).await,
684
+ Err(SignerError::Internal(_))
685
+ ));
686
+ assert!(matches!(
687
+ poisoned.export_persistable_key_material(&[]),
688
+ Err(SignerError::Internal(_))
689
+ ));
690
+ assert!(matches!(
691
+ poisoned.restore_persistable_key_material(&HashMap::new()),
692
+ Err(SignerError::Internal(_))
693
+ ));
694
+ }
695
+
696
+ #[cfg(all(target_os = "macos", not(coverage)))]
697
+ #[test]
698
+ fn secure_enclave_root_requirement_helper_covers_root_and_non_root() {
699
+ assert!(SecureEnclaveSignerBackend::require_root_euid(0).is_ok());
700
+ assert!(matches!(
701
+ SecureEnclaveSignerBackend::require_root_euid(501),
702
+ Err(SignerError::PermissionDenied(_))
703
+ ));
704
+ }
530
705
 
531
706
  #[tokio::test]
532
707
  async fn generated_key_can_sign_payload() {
@@ -611,7 +786,68 @@ mod tests {
611
786
  assert!(!sig.bytes.is_empty());
612
787
  }
613
788
 
614
- #[cfg(target_os = "macos")]
789
+ #[tokio::test]
790
+ async fn software_signer_helpers_cover_public_key_and_invalid_restore_paths() {
791
+ let backend = SoftwareSignerBackend::default();
792
+ let key = backend
793
+ .create_vault_key(KeyCreateRequest::Generate)
794
+ .await
795
+ .expect("must create key");
796
+
797
+ let stored = backend.keys.read().expect("read keys");
798
+ let signing_key = stored.get(&key.id).expect("stored signing key");
799
+ assert_eq!(SoftwareSignerBackend::public_key_hex(signing_key), key.public_key_hex);
800
+ drop(stored);
801
+
802
+ let imported = SoftwareSignerBackend::parse_import_key(&format!("0x{}", "22".repeat(32)))
803
+ .expect("must parse prefixed import key");
804
+ assert_eq!(imported.to_bytes().len(), 32);
805
+ assert!(matches!(
806
+ SoftwareSignerBackend::parse_import_key("not-hex"),
807
+ Err(SignerError::InvalidPrivateKey)
808
+ ));
809
+ assert!(matches!(
810
+ backend.restore_persistable_key_material(&HashMap::from([(
811
+ Uuid::new_v4(),
812
+ "not-hex".to_string()
813
+ )])),
814
+ Err(SignerError::InvalidPrivateKey)
815
+ ));
816
+ assert_eq!(
817
+ backend
818
+ .export_persistable_key_material(&[])
819
+ .expect("empty export"),
820
+ HashMap::new()
821
+ );
822
+ }
823
+
824
+ #[cfg(any(not(target_os = "macos"), coverage))]
825
+ #[tokio::test]
826
+ async fn secure_enclave_backend_is_explicitly_unsupported_off_macos() {
827
+ let backend = SecureEnclaveSignerBackend::default();
828
+ assert!(matches!(
829
+ backend.create_vault_key(KeyCreateRequest::Generate).await,
830
+ Err(SignerError::Unsupported(message)) if message.contains("requires macOS")
831
+ ));
832
+ assert!(matches!(
833
+ backend
834
+ .create_vault_key(KeyCreateRequest::Import {
835
+ private_key_hex: "11".repeat(32)
836
+ })
837
+ .await,
838
+ Err(SignerError::Unsupported(message)) if message.contains("requires macOS")
839
+ ));
840
+ assert!(matches!(
841
+ backend.sign_payload(Uuid::new_v4(), b"payload").await,
842
+ Err(SignerError::Unsupported(message)) if message.contains("requires macOS")
843
+ ));
844
+ assert!(matches!(
845
+ backend.sign_digest(Uuid::new_v4(), [7u8; 32]).await,
846
+ Err(SignerError::Unsupported(message)) if message.contains("requires macOS")
847
+ ));
848
+ }
849
+
850
+ #[cfg(all(target_os = "macos", not(coverage)))]
615
851
  #[tokio::test]
616
852
  async fn secure_enclave_import_is_explicitly_unsupported() {
617
853
  use super::SecureEnclaveSignerBackend;
@@ -626,14 +862,10 @@ mod tests {
626
862
  assert!(matches!(result, Err(SignerError::Unsupported(_))));
627
863
  }
628
864
 
629
- #[cfg(target_os = "macos")]
865
+ #[cfg(all(target_os = "macos", not(coverage)))]
630
866
  #[tokio::test]
631
867
  async fn secure_enclave_generate_requires_root_context() {
632
- use super::SecureEnclaveSignerBackend;
633
-
634
- if unsafe { libc::geteuid() } == 0 {
635
- return;
636
- }
868
+ assert_ne!(unsafe { libc::geteuid() }, 0, "coverage test expects non-root runtime");
637
869
 
638
870
  let backend = SecureEnclaveSignerBackend::default();
639
871
  let result = backend.create_vault_key(KeyCreateRequest::Generate).await;
@@ -641,15 +873,10 @@ mod tests {
641
873
  assert!(matches!(result, Err(SignerError::PermissionDenied(_))));
642
874
  }
643
875
 
644
- #[cfg(target_os = "macos")]
876
+ #[cfg(all(target_os = "macos", not(coverage)))]
645
877
  #[tokio::test]
646
878
  async fn secure_enclave_sign_requires_root_context() {
647
- use super::SecureEnclaveSignerBackend;
648
- use uuid::Uuid;
649
-
650
- if unsafe { libc::geteuid() } == 0 {
651
- return;
652
- }
879
+ assert_ne!(unsafe { libc::geteuid() }, 0, "coverage test expects non-root runtime");
653
880
 
654
881
  let backend = SecureEnclaveSignerBackend::default();
655
882
  let result = backend.sign_payload(Uuid::new_v4(), b"payload").await;
@@ -657,8 +884,7 @@ mod tests {
657
884
  assert!(matches!(result, Err(SignerError::PermissionDenied(_))));
658
885
  }
659
886
 
660
- #[cfg(target_os = "macos")]
661
- #[ignore = "requires a logged-in, entitlement-capable keychain session"]
887
+ #[cfg(all(target_os = "macos", not(coverage), feature = "interactive-secure-enclave-tests"))]
662
888
  #[tokio::test]
663
889
  async fn secure_enclave_can_generate_and_sign() {
664
890
  use core_foundation::base::{TCFType, ToVoid};