@wlfi-agent/cli 1.4.13 → 1.4.15

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 (289) hide show
  1. package/Cargo.lock +3968 -0
  2. package/Cargo.toml +50 -0
  3. package/README.md +426 -6
  4. package/crates/vault-cli-admin/Cargo.toml +26 -0
  5. package/crates/vault-cli-admin/src/io_utils.rs +500 -0
  6. package/crates/vault-cli-admin/src/main.rs +3990 -0
  7. package/crates/vault-cli-admin/src/shared_config.rs +624 -0
  8. package/crates/vault-cli-admin/src/tui/amounts.rs +180 -0
  9. package/crates/vault-cli-admin/src/tui/token_rpc.rs +250 -0
  10. package/crates/vault-cli-admin/src/tui/utils.rs +82 -0
  11. package/crates/vault-cli-admin/src/tui.rs +3410 -0
  12. package/crates/vault-cli-agent/Cargo.toml +24 -0
  13. package/crates/vault-cli-agent/src/io_utils.rs +576 -0
  14. package/crates/vault-cli-agent/src/main.rs +833 -0
  15. package/crates/vault-cli-daemon/Cargo.toml +28 -0
  16. package/crates/vault-cli-daemon/src/bin/wlfi-agent-system-keychain.rs +216 -0
  17. package/crates/vault-cli-daemon/src/main.rs +644 -0
  18. package/crates/vault-cli-daemon/src/relay_sync.rs +894 -0
  19. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +167 -0
  20. package/crates/vault-daemon/Cargo.toml +32 -0
  21. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +1041 -0
  22. package/crates/vault-daemon/src/daemon_parts/core_helpers.rs +1256 -0
  23. package/crates/vault-daemon/src/daemon_parts/types_api_rpc.rs +622 -0
  24. package/crates/vault-daemon/src/lib.rs +54 -0
  25. package/crates/vault-daemon/src/persistence.rs +441 -0
  26. package/crates/vault-daemon/src/tests.rs +237 -0
  27. package/crates/vault-daemon/src/tests_parts/part1.rs +1224 -0
  28. package/crates/vault-daemon/src/tests_parts/part2.rs +1021 -0
  29. package/crates/vault-daemon/src/tests_parts/part3.rs +835 -0
  30. package/crates/vault-daemon/src/tests_parts/part4.rs +604 -0
  31. package/crates/vault-domain/Cargo.toml +20 -0
  32. package/crates/vault-domain/src/action.rs +849 -0
  33. package/crates/vault-domain/src/address.rs +51 -0
  34. package/crates/vault-domain/src/approval.rs +90 -0
  35. package/crates/vault-domain/src/constants.rs +4 -0
  36. package/crates/vault-domain/src/error.rs +54 -0
  37. package/crates/vault-domain/src/keys.rs +71 -0
  38. package/crates/vault-domain/src/lib.rs +42 -0
  39. package/crates/vault-domain/src/nonce.rs +102 -0
  40. package/crates/vault-domain/src/policy.rs +172 -0
  41. package/crates/vault-domain/src/request.rs +53 -0
  42. package/crates/vault-domain/src/scope.rs +24 -0
  43. package/crates/vault-domain/src/session.rs +50 -0
  44. package/crates/vault-domain/src/signature.rs +34 -0
  45. package/crates/vault-domain/src/tests.rs +651 -0
  46. package/crates/vault-domain/src/u128_as_decimal_string.rs +44 -0
  47. package/crates/vault-policy/Cargo.toml +17 -0
  48. package/crates/vault-policy/src/engine.rs +301 -0
  49. package/crates/vault-policy/src/error.rs +81 -0
  50. package/crates/vault-policy/src/lib.rs +17 -0
  51. package/crates/vault-policy/src/report.rs +34 -0
  52. package/crates/vault-policy/src/tests.rs +891 -0
  53. package/crates/vault-policy/src/tests_explain.rs +78 -0
  54. package/crates/vault-sdk-agent/Cargo.toml +21 -0
  55. package/crates/vault-sdk-agent/src/lib.rs +711 -0
  56. package/crates/vault-signer/Cargo.toml +25 -0
  57. package/crates/vault-signer/src/lib.rs +731 -0
  58. package/crates/vault-signer/tests/secure_enclave_acl.rs +54 -0
  59. package/crates/vault-transport-unix/Cargo.toml +24 -0
  60. package/crates/vault-transport-unix/src/lib.rs +1640 -0
  61. package/crates/vault-transport-xpc/Cargo.toml +25 -0
  62. package/crates/vault-transport-xpc/src/client_codec_api.rs +635 -0
  63. package/crates/vault-transport-xpc/src/lib.rs +680 -0
  64. package/crates/vault-transport-xpc/src/tests.rs +818 -0
  65. package/crates/vault-transport-xpc/tests/e2e_flow.rs +773 -0
  66. package/dist/cli.cjs +35088 -0
  67. package/dist/cli.cjs.map +1 -0
  68. package/package.json +45 -41
  69. package/packages/cache/.turbo/turbo-build.log +52 -0
  70. package/packages/cache/dist/chunk-2QFWMUXT.cjs +43 -0
  71. package/packages/cache/dist/chunk-2QFWMUXT.cjs.map +1 -0
  72. package/packages/cache/dist/chunk-4U63TZTQ.js +43 -0
  73. package/packages/cache/dist/chunk-4U63TZTQ.js.map +1 -0
  74. package/packages/cache/dist/chunk-ALQ6H7KG.cjs +404 -0
  75. package/packages/cache/dist/chunk-ALQ6H7KG.cjs.map +1 -0
  76. package/packages/cache/dist/chunk-FGJEEF5N.js +404 -0
  77. package/packages/cache/dist/chunk-FGJEEF5N.js.map +1 -0
  78. package/packages/cache/dist/chunk-UYNEHZHB.cjs +45 -0
  79. package/packages/cache/dist/chunk-UYNEHZHB.cjs.map +1 -0
  80. package/packages/cache/dist/chunk-VXVMPG3W.js +45 -0
  81. package/packages/cache/dist/chunk-VXVMPG3W.js.map +1 -0
  82. package/packages/cache/dist/client/index.cjs +11 -0
  83. package/packages/cache/dist/client/index.cjs.map +1 -0
  84. package/packages/cache/dist/client/index.d.cts +15 -0
  85. package/packages/cache/dist/client/index.d.ts +15 -0
  86. package/packages/cache/dist/client/index.js +11 -0
  87. package/packages/cache/dist/client/index.js.map +1 -0
  88. package/packages/cache/dist/errors/index.cjs +11 -0
  89. package/packages/cache/dist/errors/index.cjs.map +1 -0
  90. package/packages/cache/dist/errors/index.d.cts +26 -0
  91. package/packages/cache/dist/errors/index.d.ts +26 -0
  92. package/packages/cache/dist/errors/index.js +11 -0
  93. package/packages/cache/dist/errors/index.js.map +1 -0
  94. package/packages/cache/dist/index.cjs +29 -0
  95. package/packages/cache/dist/index.cjs.map +1 -0
  96. package/packages/cache/dist/index.d.cts +4 -0
  97. package/packages/cache/dist/index.d.ts +4 -0
  98. package/packages/cache/dist/index.js +29 -0
  99. package/packages/cache/dist/index.js.map +1 -0
  100. package/packages/cache/dist/service/index.cjs +15 -0
  101. package/packages/cache/dist/service/index.cjs.map +1 -0
  102. package/packages/cache/dist/service/index.d.cts +184 -0
  103. package/packages/cache/dist/service/index.d.ts +184 -0
  104. package/packages/cache/dist/service/index.js +15 -0
  105. package/packages/cache/dist/service/index.js.map +1 -0
  106. package/packages/cache/node_modules/.bin/jiti +17 -0
  107. package/packages/cache/node_modules/.bin/tsc +17 -0
  108. package/packages/cache/node_modules/.bin/tsserver +17 -0
  109. package/packages/cache/node_modules/.bin/tsup +17 -0
  110. package/packages/cache/node_modules/.bin/tsup-node +17 -0
  111. package/packages/cache/node_modules/.bin/tsx +17 -0
  112. package/packages/cache/node_modules/.bin/vitest +17 -0
  113. package/packages/cache/package.json +48 -0
  114. package/packages/cache/src/client/index.ts +56 -0
  115. package/packages/cache/src/errors/index.ts +53 -0
  116. package/packages/cache/src/index.ts +3 -0
  117. package/packages/cache/src/service/index.test.ts +263 -0
  118. package/packages/cache/src/service/index.ts +678 -0
  119. package/packages/cache/tsconfig.json +13 -0
  120. package/packages/cache/tsup.config.ts +13 -0
  121. package/packages/cache/vitest.config.ts +16 -0
  122. package/packages/config/.turbo/turbo-build.log +18 -0
  123. package/packages/config/dist/index.cjs +1037 -0
  124. package/packages/config/dist/index.cjs.map +1 -0
  125. package/packages/config/dist/index.d.ts +131 -0
  126. package/packages/config/node_modules/.bin/jiti +17 -0
  127. package/packages/config/node_modules/.bin/tsc +17 -0
  128. package/packages/config/node_modules/.bin/tsserver +17 -0
  129. package/packages/config/node_modules/.bin/tsup +17 -0
  130. package/packages/config/node_modules/.bin/tsup-node +17 -0
  131. package/packages/config/node_modules/.bin/tsx +17 -0
  132. package/packages/config/package.json +21 -0
  133. package/packages/config/src/index.js +1 -0
  134. package/packages/config/src/index.ts +1282 -0
  135. package/packages/config/tsconfig.json +4 -0
  136. package/packages/rpc/.turbo/turbo-build.log +32 -0
  137. package/packages/rpc/dist/_esm-BCLXDO2R.cjs +3660 -0
  138. package/packages/rpc/dist/_esm-BCLXDO2R.cjs.map +1 -0
  139. package/packages/rpc/dist/ccip-OWJLAW55.cjs +16 -0
  140. package/packages/rpc/dist/ccip-OWJLAW55.cjs.map +1 -0
  141. package/packages/rpc/dist/chunk-APQIFZ3B.cjs +6247 -0
  142. package/packages/rpc/dist/chunk-APQIFZ3B.cjs.map +1 -0
  143. package/packages/rpc/dist/chunk-CDO2GWRD.cjs +410 -0
  144. package/packages/rpc/dist/chunk-CDO2GWRD.cjs.map +1 -0
  145. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs +2249 -0
  146. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs.map +1 -0
  147. package/packages/rpc/dist/chunk-TZDTAHWR.cjs +44 -0
  148. package/packages/rpc/dist/chunk-TZDTAHWR.cjs.map +1 -0
  149. package/packages/rpc/dist/index.cjs +7342 -0
  150. package/packages/rpc/dist/index.cjs.map +1 -0
  151. package/packages/rpc/dist/index.d.ts +3857 -0
  152. package/packages/rpc/dist/secp256k1-WCNM675D.cjs +18 -0
  153. package/packages/rpc/dist/secp256k1-WCNM675D.cjs.map +1 -0
  154. package/packages/rpc/node_modules/.bin/jiti +17 -0
  155. package/packages/rpc/node_modules/.bin/tsc +17 -0
  156. package/packages/rpc/node_modules/.bin/tsserver +17 -0
  157. package/packages/rpc/node_modules/.bin/tsup +17 -0
  158. package/packages/rpc/node_modules/.bin/tsup-node +17 -0
  159. package/packages/rpc/node_modules/.bin/tsx +17 -0
  160. package/packages/rpc/package.json +25 -0
  161. package/packages/rpc/src/index.ts +206 -0
  162. package/packages/rpc/tsconfig.json +4 -0
  163. package/packages/typescript/base.json +36 -0
  164. package/packages/typescript/nextjs.json +17 -0
  165. package/packages/typescript/package.json +10 -0
  166. package/packages/ui/.turbo/turbo-build.log +44 -0
  167. package/packages/ui/dist/chunk-MOAFBKSA.js +11 -0
  168. package/packages/ui/dist/chunk-MOAFBKSA.js.map +1 -0
  169. package/packages/ui/dist/components/badge.d.ts +12 -0
  170. package/packages/ui/dist/components/badge.js +31 -0
  171. package/packages/ui/dist/components/badge.js.map +1 -0
  172. package/packages/ui/dist/components/button.d.ts +13 -0
  173. package/packages/ui/dist/components/button.js +40 -0
  174. package/packages/ui/dist/components/button.js.map +1 -0
  175. package/packages/ui/dist/components/card.d.ts +10 -0
  176. package/packages/ui/dist/components/card.js +39 -0
  177. package/packages/ui/dist/components/card.js.map +1 -0
  178. package/packages/ui/dist/components/input.d.ts +5 -0
  179. package/packages/ui/dist/components/input.js +28 -0
  180. package/packages/ui/dist/components/input.js.map +1 -0
  181. package/packages/ui/dist/components/label.d.ts +5 -0
  182. package/packages/ui/dist/components/label.js +13 -0
  183. package/packages/ui/dist/components/label.js.map +1 -0
  184. package/packages/ui/dist/components/separator.d.ts +5 -0
  185. package/packages/ui/dist/components/separator.js +13 -0
  186. package/packages/ui/dist/components/separator.js.map +1 -0
  187. package/packages/ui/dist/components/textarea.d.ts +5 -0
  188. package/packages/ui/dist/components/textarea.js +27 -0
  189. package/packages/ui/dist/components/textarea.js.map +1 -0
  190. package/packages/ui/dist/tailwind.d.ts +56 -0
  191. package/packages/ui/dist/tailwind.js +60 -0
  192. package/packages/ui/dist/tailwind.js.map +1 -0
  193. package/packages/ui/dist/utils/cn.d.ts +5 -0
  194. package/packages/ui/dist/utils/cn.js +7 -0
  195. package/packages/ui/dist/utils/cn.js.map +1 -0
  196. package/packages/ui/node_modules/.bin/jiti +17 -0
  197. package/packages/ui/node_modules/.bin/tsc +17 -0
  198. package/packages/ui/node_modules/.bin/tsserver +17 -0
  199. package/packages/ui/node_modules/.bin/tsup +17 -0
  200. package/packages/ui/node_modules/.bin/tsup-node +17 -0
  201. package/packages/ui/node_modules/.bin/tsx +17 -0
  202. package/packages/ui/package.json +69 -0
  203. package/packages/ui/src/components/badge.tsx +27 -0
  204. package/packages/ui/src/components/button.tsx +40 -0
  205. package/packages/ui/src/components/card.tsx +31 -0
  206. package/packages/ui/src/components/input.tsx +21 -0
  207. package/packages/ui/src/components/label.tsx +6 -0
  208. package/packages/ui/src/components/separator.tsx +6 -0
  209. package/packages/ui/src/components/textarea.tsx +20 -0
  210. package/packages/ui/src/globals.css +70 -0
  211. package/packages/ui/src/tailwind.ts +56 -0
  212. package/packages/ui/src/utils/cn.ts +6 -0
  213. package/packages/ui/tsconfig.json +20 -0
  214. package/packages/ui/tsup.config.ts +20 -0
  215. package/pnpm-workspace.yaml +4 -0
  216. package/scripts/install-rust-binaries.mjs +84 -0
  217. package/scripts/launchd/install-user-daemon.sh +358 -0
  218. package/scripts/launchd/run-vault-daemon.sh +5 -0
  219. package/scripts/launchd/run-wlfi-agent-daemon.sh +73 -0
  220. package/scripts/launchd/uninstall-user-daemon.sh +103 -0
  221. package/src/cli.ts +2121 -0
  222. package/src/lib/admin-guard.js +1 -0
  223. package/src/lib/admin-guard.ts +185 -0
  224. package/src/lib/admin-passthrough.ts +33 -0
  225. package/src/lib/admin-reset.ts +751 -0
  226. package/src/lib/admin-setup.ts +1612 -0
  227. package/src/lib/agent-auth-clear.js +1 -0
  228. package/src/lib/agent-auth-clear.ts +58 -0
  229. package/src/lib/agent-auth-forwarding.js +1 -0
  230. package/src/lib/agent-auth-forwarding.ts +149 -0
  231. package/src/lib/agent-auth-migrate.js +1 -0
  232. package/src/lib/agent-auth-migrate.ts +150 -0
  233. package/src/lib/agent-auth-revoke.ts +103 -0
  234. package/src/lib/agent-auth-rotate.ts +107 -0
  235. package/src/lib/agent-auth-token.js +1 -0
  236. package/src/lib/agent-auth-token.ts +25 -0
  237. package/src/lib/agent-auth.ts +89 -0
  238. package/src/lib/asset-broadcast.js +1 -0
  239. package/src/lib/asset-broadcast.ts +285 -0
  240. package/src/lib/bootstrap-artifacts.js +1 -0
  241. package/src/lib/bootstrap-artifacts.ts +205 -0
  242. package/src/lib/bootstrap-credentials.js +1 -0
  243. package/src/lib/bootstrap-credentials.ts +832 -0
  244. package/src/lib/config-amounts.js +1 -0
  245. package/src/lib/config-amounts.ts +189 -0
  246. package/src/lib/config-mutation.ts +27 -0
  247. package/src/lib/fs-trust.js +1 -0
  248. package/src/lib/fs-trust.ts +537 -0
  249. package/src/lib/keychain.js +1 -0
  250. package/src/lib/keychain.ts +225 -0
  251. package/src/lib/local-admin-access.ts +106 -0
  252. package/src/lib/network-selection.js +1 -0
  253. package/src/lib/network-selection.ts +71 -0
  254. package/src/lib/passthrough-security.js +1 -0
  255. package/src/lib/passthrough-security.ts +114 -0
  256. package/src/lib/rpc-guard.js +1 -0
  257. package/src/lib/rpc-guard.ts +7 -0
  258. package/src/lib/rust-spawn-options.js +1 -0
  259. package/src/lib/rust-spawn-options.ts +98 -0
  260. package/src/lib/rust.js +1 -0
  261. package/src/lib/rust.ts +143 -0
  262. package/src/lib/signed-tx.js +1 -0
  263. package/src/lib/signed-tx.ts +116 -0
  264. package/src/lib/status-repair-cli.ts +116 -0
  265. package/src/lib/sudo.js +1 -0
  266. package/src/lib/sudo.ts +172 -0
  267. package/src/lib/vault-password-forwarding.js +1 -0
  268. package/src/lib/vault-password-forwarding.ts +155 -0
  269. package/src/lib/wallet-profile.js +1 -0
  270. package/src/lib/wallet-profile.ts +332 -0
  271. package/src/lib/wallet-repair.js +1 -0
  272. package/src/lib/wallet-repair.ts +304 -0
  273. package/src/lib/wallet-setup.js +1 -0
  274. package/src/lib/wallet-setup.ts +1466 -0
  275. package/src/lib/wallet-status.js +1 -0
  276. package/src/lib/wallet-status.ts +640 -0
  277. package/tsconfig.base.json +17 -0
  278. package/tsconfig.json +10 -0
  279. package/tsup.config.ts +25 -0
  280. package/turbo.json +41 -0
  281. package/LICENSE.md +0 -1
  282. package/dist/wlfa/index.cjs +0 -250
  283. package/dist/wlfa/index.d.cts +0 -1
  284. package/dist/wlfa/index.d.ts +0 -1
  285. package/dist/wlfa/index.js +0 -250
  286. package/dist/wlfc/index.cjs +0 -1839
  287. package/dist/wlfc/index.d.cts +0 -1
  288. package/dist/wlfc/index.d.ts +0 -1
  289. package/dist/wlfc/index.js +0 -1839
@@ -0,0 +1,731 @@
1
+ //! Signer backends for vault keys.
2
+ //!
3
+ //! The daemon depends on [`VaultSignerBackend`] so hardware-backed (Secure
4
+ //! Enclave), software-backed, and future TEE-backed implementations can be
5
+ //! swapped without changing policy logic.
6
+
7
+ use std::collections::HashMap;
8
+ use std::sync::{Arc, RwLock};
9
+
10
+ use async_trait::async_trait;
11
+ use k256::ecdsa::signature::Signer;
12
+ use k256::ecdsa::{Signature as EcdsaSignature, SigningKey};
13
+ use k256::elliptic_curve::rand_core::OsRng;
14
+ use serde::{Deserialize, Serialize};
15
+ use thiserror::Error;
16
+ use time::OffsetDateTime;
17
+ use uuid::Uuid;
18
+ use vault_domain::{KeySource, Signature, VaultKey};
19
+
20
+ #[cfg(target_os = "macos")]
21
+ use core_foundation::base::{TCFType, ToVoid};
22
+ #[cfg(target_os = "macos")]
23
+ use core_foundation::string::CFString;
24
+ #[cfg(target_os = "macos")]
25
+ use security_framework::access_control::{ProtectionMode, SecAccessControl};
26
+ #[cfg(target_os = "macos")]
27
+ use security_framework::item::{
28
+ ItemClass, ItemSearchOptions, KeyClass, Limit, Location, Reference, SearchResult,
29
+ };
30
+ #[cfg(target_os = "macos")]
31
+ use security_framework::key::{Algorithm, GenerateKeyOptions, KeyType, SecKey, Token};
32
+ #[cfg(target_os = "macos")]
33
+ use security_framework_sys::access_control::kSecAccessControlPrivateKeyUsage;
34
+ #[cfg(target_os = "macos")]
35
+ use security_framework_sys::item::{
36
+ kSecAttrAccessControl, kSecAttrTokenID, kSecAttrTokenIDSecureEnclave,
37
+ };
38
+
39
+ /// Logical backend category.
40
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
41
+ pub enum BackendKind {
42
+ /// macOS Secure Enclave + Keychain backend.
43
+ SecureEnclave,
44
+ /// Hardware-backed remote TEE backend.
45
+ Tee,
46
+ /// In-process software signer backend.
47
+ Software,
48
+ }
49
+
50
+ /// Key creation request from daemon/admin.
51
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52
+ pub enum KeyCreateRequest {
53
+ /// Generate a fresh private key.
54
+ Generate,
55
+ /// Import an existing hex-encoded 32-byte secp256k1 private key.
56
+ Import { private_key_hex: String },
57
+ }
58
+
59
+ /// Errors returned by signer backends.
60
+ #[derive(Debug, Error, Clone, PartialEq, Eq, Serialize, Deserialize)]
61
+ pub enum SignerError {
62
+ /// Unknown key identifier.
63
+ #[error("unknown vault key id: {0}")]
64
+ UnknownKey(Uuid),
65
+ /// Invalid import key material.
66
+ #[error("invalid private key")]
67
+ InvalidPrivateKey,
68
+ /// Operation is intentionally unsupported by backend.
69
+ #[error("backend operation unsupported: {0}")]
70
+ Unsupported(String),
71
+ /// Caller does not satisfy backend security requirements.
72
+ #[error("permission denied: {0}")]
73
+ PermissionDenied(String),
74
+ /// Backend-specific failure.
75
+ #[error("internal backend failure: {0}")]
76
+ Internal(String),
77
+ }
78
+
79
+ /// Backend interface used by daemon.
80
+ #[async_trait]
81
+ pub trait VaultSignerBackend: Send + Sync {
82
+ /// Returns backend category.
83
+ fn backend_kind(&self) -> BackendKind;
84
+
85
+ /// Creates a vault key according to request.
86
+ async fn create_vault_key(&self, request: KeyCreateRequest) -> Result<VaultKey, SignerError>;
87
+
88
+ /// Signs payload with key `vault_key_id`.
89
+ async fn sign_payload(
90
+ &self,
91
+ vault_key_id: Uuid,
92
+ payload: &[u8],
93
+ ) -> Result<Signature, SignerError>;
94
+
95
+ /// Signs a prehashed 32-byte digest with key `vault_key_id`.
96
+ ///
97
+ /// Digest format is caller-defined; for Ethereum transactions this must be
98
+ /// Keccak-256 transaction-signing prehash.
99
+ async fn sign_digest(
100
+ &self,
101
+ vault_key_id: Uuid,
102
+ digest: [u8; 32],
103
+ ) -> Result<Signature, SignerError>;
104
+
105
+ /// Exports persistable key material for the requested vault key IDs.
106
+ ///
107
+ /// Backends that keep private keys outside daemon persistence (for example
108
+ /// Secure Enclave) should return an empty map.
109
+ fn export_persistable_key_material(
110
+ &self,
111
+ _vault_key_ids: &[Uuid],
112
+ ) -> Result<HashMap<Uuid, String>, SignerError> {
113
+ Ok(HashMap::new())
114
+ }
115
+
116
+ /// Restores persistable key material previously exported by this backend.
117
+ ///
118
+ /// Backends that do not support material export/import return an error when
119
+ /// non-empty state is provided.
120
+ fn restore_persistable_key_material(
121
+ &self,
122
+ persisted: &HashMap<Uuid, String>,
123
+ ) -> Result<(), SignerError> {
124
+ if persisted.is_empty() {
125
+ return Ok(());
126
+ }
127
+ Err(SignerError::Unsupported(
128
+ "backend does not support persisted key material".to_string(),
129
+ ))
130
+ }
131
+ }
132
+
133
+ /// Optional extension for backends capable of cryptographic attestation.
134
+ #[async_trait]
135
+ pub trait AttestableSignerBackend: VaultSignerBackend {
136
+ /// Returns an attestation document proving key/backend identity.
137
+ async fn attestation_document(&self) -> Result<Vec<u8>, SignerError>;
138
+ }
139
+
140
+ /// Pure software signer for development and tests.
141
+ #[derive(Debug, Clone, Default)]
142
+ pub struct SoftwareSignerBackend {
143
+ keys: Arc<RwLock<HashMap<Uuid, SigningKey>>>,
144
+ }
145
+
146
+ impl SoftwareSignerBackend {
147
+ fn public_key_hex(signing_key: &SigningKey) -> String {
148
+ let verifying_key = signing_key.verifying_key();
149
+ hex::encode(verifying_key.to_encoded_point(false).as_bytes())
150
+ }
151
+
152
+ fn parse_import_key(private_key_hex: &str) -> Result<SigningKey, SignerError> {
153
+ let raw = hex::decode(
154
+ private_key_hex
155
+ .strip_prefix("0x")
156
+ .unwrap_or(private_key_hex),
157
+ )
158
+ .map_err(|_| SignerError::InvalidPrivateKey)?;
159
+ if raw.len() != 32 {
160
+ return Err(SignerError::InvalidPrivateKey);
161
+ }
162
+ SigningKey::from_slice(&raw).map_err(|_| SignerError::InvalidPrivateKey)
163
+ }
164
+ }
165
+
166
+ #[async_trait]
167
+ impl VaultSignerBackend for SoftwareSignerBackend {
168
+ fn backend_kind(&self) -> BackendKind {
169
+ BackendKind::Software
170
+ }
171
+
172
+ async fn create_vault_key(&self, request: KeyCreateRequest) -> Result<VaultKey, SignerError> {
173
+ let (signing_key, source) = match request {
174
+ KeyCreateRequest::Generate => (SigningKey::random(&mut OsRng), KeySource::Generated),
175
+ KeyCreateRequest::Import { private_key_hex } => {
176
+ let key = Self::parse_import_key(&private_key_hex)?;
177
+ (key, KeySource::Imported)
178
+ }
179
+ };
180
+
181
+ let key_id = Uuid::new_v4();
182
+ let public_key_hex = Self::public_key_hex(&signing_key);
183
+ let created_at = OffsetDateTime::now_utc();
184
+
185
+ self.keys
186
+ .write()
187
+ .map_err(|_| SignerError::Internal("poisoned lock".into()))?
188
+ .insert(key_id, signing_key);
189
+
190
+ Ok(VaultKey {
191
+ id: key_id,
192
+ source,
193
+ public_key_hex,
194
+ created_at,
195
+ })
196
+ }
197
+
198
+ async fn sign_payload(
199
+ &self,
200
+ vault_key_id: Uuid,
201
+ payload: &[u8],
202
+ ) -> Result<Signature, SignerError> {
203
+ let keys = self
204
+ .keys
205
+ .read()
206
+ .map_err(|_| SignerError::Internal("poisoned lock".into()))?;
207
+ let signing_key = keys
208
+ .get(&vault_key_id)
209
+ .ok_or(SignerError::UnknownKey(vault_key_id))?;
210
+
211
+ let signature: EcdsaSignature = signing_key.sign(payload);
212
+ Ok(Signature::from_der(signature.to_der().as_bytes().to_vec()))
213
+ }
214
+
215
+ async fn sign_digest(
216
+ &self,
217
+ vault_key_id: Uuid,
218
+ digest: [u8; 32],
219
+ ) -> Result<Signature, SignerError> {
220
+ let keys = self
221
+ .keys
222
+ .read()
223
+ .map_err(|_| SignerError::Internal("poisoned lock".into()))?;
224
+ let signing_key = keys
225
+ .get(&vault_key_id)
226
+ .ok_or(SignerError::UnknownKey(vault_key_id))?;
227
+
228
+ let (signature, _) = signing_key
229
+ .sign_prehash_recoverable(&digest)
230
+ .map_err(|err| SignerError::Internal(format!("digest signature failed: {err}")))?;
231
+ Ok(Signature::from_der(signature.to_der().as_bytes().to_vec()))
232
+ }
233
+
234
+ fn export_persistable_key_material(
235
+ &self,
236
+ vault_key_ids: &[Uuid],
237
+ ) -> Result<HashMap<Uuid, String>, SignerError> {
238
+ let keys = self
239
+ .keys
240
+ .read()
241
+ .map_err(|_| SignerError::Internal("poisoned lock".into()))?;
242
+ let mut exported = HashMap::with_capacity(vault_key_ids.len());
243
+ for vault_key_id in vault_key_ids {
244
+ let signing_key = keys
245
+ .get(vault_key_id)
246
+ .ok_or(SignerError::UnknownKey(*vault_key_id))?;
247
+ exported.insert(*vault_key_id, hex::encode(signing_key.to_bytes()));
248
+ }
249
+ Ok(exported)
250
+ }
251
+
252
+ fn restore_persistable_key_material(
253
+ &self,
254
+ persisted: &HashMap<Uuid, String>,
255
+ ) -> Result<(), SignerError> {
256
+ let mut restored = HashMap::with_capacity(persisted.len());
257
+ for (vault_key_id, private_key_hex) in persisted {
258
+ let signing_key = Self::parse_import_key(private_key_hex)?;
259
+ restored.insert(*vault_key_id, signing_key);
260
+ }
261
+ *self
262
+ .keys
263
+ .write()
264
+ .map_err(|_| SignerError::Internal("poisoned lock".into()))? = restored;
265
+ Ok(())
266
+ }
267
+ }
268
+
269
+ /// Real macOS Secure Enclave signer.
270
+ ///
271
+ /// Generated keys are permanent Keychain items with `PRIVATE_KEY_USAGE`
272
+ /// access control and `kSecAttrTokenIDSecureEnclave` token binding.
273
+ #[derive(Debug, Clone)]
274
+ pub struct SecureEnclaveSignerBackend {
275
+ label_prefix: String,
276
+ }
277
+
278
+ impl Default for SecureEnclaveSignerBackend {
279
+ fn default() -> Self {
280
+ Self::new("com.wlfi.vault")
281
+ }
282
+ }
283
+
284
+ impl SecureEnclaveSignerBackend {
285
+ /// Creates backend with a key label prefix.
286
+ #[must_use]
287
+ pub fn new(label_prefix: impl Into<String>) -> Self {
288
+ Self {
289
+ label_prefix: label_prefix.into(),
290
+ }
291
+ }
292
+
293
+ fn key_label(&self, key_id: Uuid) -> String {
294
+ format!("{}.{key_id}", self.label_prefix)
295
+ }
296
+
297
+ #[cfg(target_os = "macos")]
298
+ fn require_root() -> Result<(), SignerError> {
299
+ if unsafe { libc::geteuid() } != 0 {
300
+ return Err(SignerError::PermissionDenied(
301
+ "secure enclave backend requires root daemon context".to_string(),
302
+ ));
303
+ }
304
+ Ok(())
305
+ }
306
+
307
+ #[cfg(target_os = "macos")]
308
+ fn make_access_control() -> Result<SecAccessControl, SignerError> {
309
+ SecAccessControl::create_with_protection(
310
+ Some(ProtectionMode::AccessibleAfterFirstUnlockThisDeviceOnly),
311
+ kSecAccessControlPrivateKeyUsage,
312
+ )
313
+ .map_err(|err| SignerError::Internal(format!("unable to create access control: {err}")))
314
+ }
315
+
316
+ #[cfg(target_os = "macos")]
317
+ fn generate_secure_enclave_key(&self, key_id: Uuid) -> Result<SecKey, SignerError> {
318
+ let label = self.key_label(key_id);
319
+ let mut options = GenerateKeyOptions::default();
320
+ options
321
+ .set_key_type(KeyType::ec_sec_prime_random())
322
+ .set_size_in_bits(256)
323
+ .set_label(label)
324
+ .set_token(Token::SecureEnclave)
325
+ .set_location(Location::DataProtectionKeychain)
326
+ .set_access_control(Self::make_access_control()?);
327
+
328
+ SecKey::new(&options).map_err(|err| {
329
+ SignerError::Internal(format!("secure enclave key generation failed: {err}"))
330
+ })
331
+ }
332
+
333
+ #[cfg(target_os = "macos")]
334
+ fn find_private_key(&self, key_id: Uuid) -> Result<SecKey, SignerError> {
335
+ let label = self.key_label(key_id);
336
+ let mut search = ItemSearchOptions::new();
337
+ search
338
+ .class(ItemClass::key())
339
+ .key_class(KeyClass::private())
340
+ .label(&label)
341
+ .load_refs(true)
342
+ .limit(Limit::All)
343
+ .ignore_legacy_keychains();
344
+
345
+ let mut results = search
346
+ .search()
347
+ .map_err(|err| SignerError::Internal(format!("key lookup failed: {err}")))?;
348
+ if results.is_empty() {
349
+ return Err(SignerError::UnknownKey(key_id));
350
+ }
351
+ if results.len() > 1 {
352
+ return Err(SignerError::Internal(format!(
353
+ "multiple keychain private keys matched label for vault key {key_id}; refusing ambiguous lookup"
354
+ )));
355
+ }
356
+
357
+ let first = results
358
+ .pop()
359
+ .ok_or_else(|| SignerError::Internal("missing search result".to_string()))?;
360
+
361
+ match first {
362
+ SearchResult::Ref(Reference::Key(key)) => {
363
+ Self::validate_secure_enclave_key_attributes(&key, key_id)?;
364
+ Ok(key)
365
+ }
366
+ _ => Err(SignerError::Internal(
367
+ "unexpected keychain search result type".to_string(),
368
+ )),
369
+ }
370
+ }
371
+
372
+ #[cfg(target_os = "macos")]
373
+ fn validate_secure_enclave_key_attributes(
374
+ key: &SecKey,
375
+ key_id: Uuid,
376
+ ) -> Result<(), SignerError> {
377
+ let attrs = key.attributes();
378
+ let token_attr = attrs
379
+ .find(unsafe { kSecAttrTokenID }.to_void())
380
+ .ok_or_else(|| {
381
+ SignerError::Internal(format!(
382
+ "resolved key for vault key {key_id} is missing token-id attribute"
383
+ ))
384
+ })?;
385
+ let token_value = format!("{}", unsafe {
386
+ CFString::wrap_under_get_rule(token_attr.cast())
387
+ });
388
+ let expected_secure_enclave_token = format!("{}", unsafe {
389
+ CFString::wrap_under_get_rule(kSecAttrTokenIDSecureEnclave)
390
+ });
391
+ if token_value != expected_secure_enclave_token {
392
+ return Err(SignerError::Internal(format!(
393
+ "resolved key for vault key {key_id} is not secure-enclave backed"
394
+ )));
395
+ }
396
+
397
+ let access_control = attrs
398
+ .find(unsafe { kSecAttrAccessControl }.to_void())
399
+ .ok_or_else(|| {
400
+ SignerError::Internal(format!(
401
+ "resolved key for vault key {key_id} is missing access-control metadata"
402
+ ))
403
+ })?;
404
+ if access_control.is_null() {
405
+ return Err(SignerError::Internal(format!(
406
+ "resolved key for vault key {key_id} has null access-control metadata"
407
+ )));
408
+ }
409
+
410
+ Ok(())
411
+ }
412
+
413
+ #[cfg(target_os = "macos")]
414
+ fn public_key_hex(private_key: &SecKey) -> Result<String, SignerError> {
415
+ let public_key = private_key
416
+ .public_key()
417
+ .ok_or_else(|| SignerError::Internal("missing public key".to_string()))?;
418
+ let data = public_key.external_representation().ok_or_else(|| {
419
+ SignerError::Internal("missing public key representation".to_string())
420
+ })?;
421
+ Ok(hex::encode(data.bytes()))
422
+ }
423
+
424
+ #[cfg(all(test, target_os = "macos"))]
425
+ fn delete_if_present(&self, key_id: Uuid) -> Result<(), SignerError> {
426
+ match self.find_private_key(key_id) {
427
+ Ok(key) => key
428
+ .delete()
429
+ .map_err(|err| SignerError::Internal(format!("key cleanup failed: {err}"))),
430
+ Err(SignerError::UnknownKey(_)) => Ok(()),
431
+ Err(other) => Err(other),
432
+ }
433
+ }
434
+ }
435
+
436
+ #[async_trait]
437
+ impl VaultSignerBackend for SecureEnclaveSignerBackend {
438
+ fn backend_kind(&self) -> BackendKind {
439
+ BackendKind::SecureEnclave
440
+ }
441
+
442
+ async fn create_vault_key(&self, request: KeyCreateRequest) -> Result<VaultKey, SignerError> {
443
+ #[cfg(not(target_os = "macos"))]
444
+ {
445
+ let _ = request;
446
+ return Err(SignerError::Unsupported(
447
+ "Secure Enclave backend requires macOS".to_string(),
448
+ ));
449
+ }
450
+
451
+ #[cfg(target_os = "macos")]
452
+ {
453
+ match request {
454
+ KeyCreateRequest::Generate => {
455
+ Self::require_root()?;
456
+ let key_id = Uuid::new_v4();
457
+ let private_key = self.generate_secure_enclave_key(key_id)?;
458
+ let public_key_hex = Self::public_key_hex(&private_key)?;
459
+ Ok(VaultKey {
460
+ id: key_id,
461
+ source: KeySource::Generated,
462
+ public_key_hex,
463
+ created_at: OffsetDateTime::now_utc(),
464
+ })
465
+ }
466
+ KeyCreateRequest::Import { .. } => Err(SignerError::Unsupported(
467
+ "Secure Enclave keys are non-importable; use a non-enclave backend for imports"
468
+ .to_string(),
469
+ )),
470
+ }
471
+ }
472
+ }
473
+
474
+ async fn sign_payload(
475
+ &self,
476
+ vault_key_id: Uuid,
477
+ payload: &[u8],
478
+ ) -> Result<Signature, SignerError> {
479
+ #[cfg(not(target_os = "macos"))]
480
+ {
481
+ let _ = (vault_key_id, payload);
482
+ return Err(SignerError::Unsupported(
483
+ "Secure Enclave backend requires macOS".to_string(),
484
+ ));
485
+ }
486
+
487
+ #[cfg(target_os = "macos")]
488
+ {
489
+ Self::require_root()?;
490
+ let private_key = self.find_private_key(vault_key_id)?;
491
+ let bytes = private_key
492
+ .create_signature(Algorithm::ECDSASignatureMessageX962SHA256, payload)
493
+ .map_err(|err| {
494
+ SignerError::Internal(format!("signature creation failed: {err}"))
495
+ })?;
496
+ Ok(Signature::from_der(bytes))
497
+ }
498
+ }
499
+
500
+ async fn sign_digest(
501
+ &self,
502
+ vault_key_id: Uuid,
503
+ digest: [u8; 32],
504
+ ) -> Result<Signature, SignerError> {
505
+ #[cfg(not(target_os = "macos"))]
506
+ {
507
+ let _ = (vault_key_id, digest);
508
+ return Err(SignerError::Unsupported(
509
+ "Secure Enclave backend requires macOS".to_string(),
510
+ ));
511
+ }
512
+
513
+ #[cfg(target_os = "macos")]
514
+ {
515
+ Self::require_root()?;
516
+ let private_key = self.find_private_key(vault_key_id)?;
517
+ let bytes = private_key
518
+ .create_signature(Algorithm::ECDSASignatureDigestX962, &digest)
519
+ .map_err(|err| {
520
+ SignerError::Internal(format!("digest signature creation failed: {err}"))
521
+ })?;
522
+ Ok(Signature::from_der(bytes))
523
+ }
524
+ }
525
+ }
526
+
527
+ #[cfg(test)]
528
+ mod tests {
529
+ use super::{KeyCreateRequest, SignerError, SoftwareSignerBackend, VaultSignerBackend};
530
+
531
+ #[tokio::test]
532
+ async fn generated_key_can_sign_payload() {
533
+ use k256::ecdsa::Signature as K256Signature;
534
+
535
+ let backend = SoftwareSignerBackend::default();
536
+ let key = backend
537
+ .create_vault_key(KeyCreateRequest::Generate)
538
+ .await
539
+ .expect("must create key");
540
+
541
+ let sig = backend
542
+ .sign_payload(key.id, b"payload")
543
+ .await
544
+ .expect("must sign");
545
+
546
+ let parsed = K256Signature::from_der(&sig.bytes).expect("must be DER");
547
+ assert!(!parsed.to_bytes().is_empty());
548
+ }
549
+
550
+ #[tokio::test]
551
+ async fn generated_key_can_sign_digest() {
552
+ use k256::ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey};
553
+
554
+ let backend = SoftwareSignerBackend::default();
555
+ let key = backend
556
+ .create_vault_key(KeyCreateRequest::Generate)
557
+ .await
558
+ .expect("must create key");
559
+
560
+ let digest = [0x42u8; 32];
561
+ let sig = backend
562
+ .sign_digest(key.id, digest)
563
+ .await
564
+ .expect("must sign digest");
565
+
566
+ let parsed = K256Signature::from_der(&sig.bytes).expect("must be DER");
567
+ let verifying_key = VerifyingKey::from_sec1_bytes(
568
+ &hex::decode(&key.public_key_hex).expect("public key hex"),
569
+ )
570
+ .expect("verifying key");
571
+ let recovery_id = RecoveryId::trial_recovery_from_prehash(&verifying_key, &digest, &parsed)
572
+ .expect("must derive recovery id");
573
+ let recovered = VerifyingKey::recover_from_prehash(&digest, &parsed, recovery_id)
574
+ .expect("must recover verifying key");
575
+ assert_eq!(recovered, verifying_key);
576
+ }
577
+
578
+ #[tokio::test]
579
+ async fn import_rejects_bad_key() {
580
+ let backend = SoftwareSignerBackend::default();
581
+ let result = backend
582
+ .create_vault_key(KeyCreateRequest::Import {
583
+ private_key_hex: "0x1234".to_string(),
584
+ })
585
+ .await;
586
+
587
+ assert!(result.is_err());
588
+ }
589
+
590
+ #[tokio::test]
591
+ async fn software_backend_can_export_and_restore_key_material() {
592
+ let backend = SoftwareSignerBackend::default();
593
+ let key = backend
594
+ .create_vault_key(KeyCreateRequest::Generate)
595
+ .await
596
+ .expect("must create key");
597
+
598
+ let exported = backend
599
+ .export_persistable_key_material(&[key.id])
600
+ .expect("must export key material");
601
+ assert!(exported.contains_key(&key.id));
602
+
603
+ let restored_backend = SoftwareSignerBackend::default();
604
+ restored_backend
605
+ .restore_persistable_key_material(&exported)
606
+ .expect("must restore key material");
607
+ let sig = restored_backend
608
+ .sign_payload(key.id, b"payload")
609
+ .await
610
+ .expect("must sign with restored key");
611
+ assert!(!sig.bytes.is_empty());
612
+ }
613
+
614
+ #[cfg(target_os = "macos")]
615
+ #[tokio::test]
616
+ async fn secure_enclave_import_is_explicitly_unsupported() {
617
+ use super::SecureEnclaveSignerBackend;
618
+
619
+ let backend = SecureEnclaveSignerBackend::default();
620
+ let result = backend
621
+ .create_vault_key(KeyCreateRequest::Import {
622
+ private_key_hex: "0x11".repeat(32),
623
+ })
624
+ .await;
625
+
626
+ assert!(matches!(result, Err(SignerError::Unsupported(_))));
627
+ }
628
+
629
+ #[cfg(target_os = "macos")]
630
+ #[tokio::test]
631
+ async fn secure_enclave_generate_requires_root_context() {
632
+ use super::SecureEnclaveSignerBackend;
633
+
634
+ if unsafe { libc::geteuid() } == 0 {
635
+ return;
636
+ }
637
+
638
+ let backend = SecureEnclaveSignerBackend::default();
639
+ let result = backend.create_vault_key(KeyCreateRequest::Generate).await;
640
+
641
+ assert!(matches!(result, Err(SignerError::PermissionDenied(_))));
642
+ }
643
+
644
+ #[cfg(target_os = "macos")]
645
+ #[tokio::test]
646
+ 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
+ }
653
+
654
+ let backend = SecureEnclaveSignerBackend::default();
655
+ let result = backend.sign_payload(Uuid::new_v4(), b"payload").await;
656
+
657
+ assert!(matches!(result, Err(SignerError::PermissionDenied(_))));
658
+ }
659
+
660
+ #[cfg(target_os = "macos")]
661
+ #[ignore = "requires a logged-in, entitlement-capable keychain session"]
662
+ #[tokio::test]
663
+ async fn secure_enclave_can_generate_and_sign() {
664
+ use core_foundation::base::{TCFType, ToVoid};
665
+ use security_framework::item::{
666
+ ItemClass, ItemSearchOptions, KeyClass, Limit, Reference, SearchResult,
667
+ };
668
+ use security_framework_sys::item::{
669
+ kSecAttrAccessControl, kSecAttrTokenID, kSecAttrTokenIDSecureEnclave,
670
+ };
671
+
672
+ use super::SecureEnclaveSignerBackend;
673
+
674
+ if unsafe { libc::geteuid() } != 0 {
675
+ return;
676
+ }
677
+
678
+ let backend = SecureEnclaveSignerBackend::new("com.wlfi.vault.test");
679
+ let key = backend
680
+ .create_vault_key(KeyCreateRequest::Generate)
681
+ .await
682
+ .expect("must create secure enclave key");
683
+
684
+ let sig = backend
685
+ .sign_payload(key.id, b"payload")
686
+ .await
687
+ .expect("must sign");
688
+ assert!(!sig.bytes.is_empty());
689
+
690
+ let label = format!("com.wlfi.vault.test.{}", key.id);
691
+ let mut search = ItemSearchOptions::new();
692
+ search
693
+ .class(ItemClass::key())
694
+ .key_class(KeyClass::private())
695
+ .label(&label)
696
+ .load_refs(true)
697
+ .limit(Limit::Max(1))
698
+ .ignore_legacy_keychains();
699
+
700
+ let results = search.search().expect("search must succeed");
701
+ assert_eq!(results.len(), 1);
702
+
703
+ let private_key = match &results[0] {
704
+ SearchResult::Ref(Reference::Key(key)) => key,
705
+ _ => panic!("unexpected key search result"),
706
+ };
707
+
708
+ let attrs = private_key.attributes();
709
+ let token = attrs
710
+ .find(unsafe { kSecAttrTokenID }.to_void())
711
+ .expect("secure enclave token id must be present");
712
+ let token_string = format!("{}", unsafe {
713
+ core_foundation::string::CFString::wrap_under_get_rule(token.cast())
714
+ });
715
+ let expected = format!("{}", unsafe {
716
+ core_foundation::string::CFString::wrap_under_get_rule(kSecAttrTokenIDSecureEnclave)
717
+ });
718
+ assert_eq!(token_string, expected);
719
+
720
+ assert!(
721
+ attrs
722
+ .find(unsafe { kSecAttrAccessControl }.to_void())
723
+ .is_some(),
724
+ "access-control metadata must be present"
725
+ );
726
+
727
+ backend
728
+ .delete_if_present(key.id)
729
+ .expect("cleanup should not fail");
730
+ }
731
+ }