@wlfi-agent/cli 1.4.17 → 1.4.18
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.
- package/Cargo.lock +5 -0
- package/README.md +61 -28
- package/crates/vault-cli-admin/src/io_utils.rs +149 -1
- package/crates/vault-cli-admin/src/main.rs +639 -16
- package/crates/vault-cli-admin/src/shared_config.rs +18 -18
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
- package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
- package/crates/vault-cli-admin/src/tui.rs +1205 -120
- package/crates/vault-cli-agent/Cargo.toml +1 -0
- package/crates/vault-cli-agent/src/io_utils.rs +163 -2
- package/crates/vault-cli-agent/src/main.rs +648 -32
- package/crates/vault-cli-daemon/Cargo.toml +4 -0
- package/crates/vault-cli-daemon/src/main.rs +617 -67
- package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
- package/crates/vault-daemon/src/persistence.rs +637 -100
- package/crates/vault-daemon/src/tests.rs +1013 -3
- package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
- package/crates/vault-domain/src/nonce.rs +4 -0
- package/crates/vault-domain/src/tests.rs +616 -0
- package/crates/vault-policy/src/engine.rs +55 -32
- package/crates/vault-policy/src/tests.rs +195 -0
- package/crates/vault-sdk-agent/src/lib.rs +415 -22
- package/crates/vault-signer/Cargo.toml +3 -0
- package/crates/vault-signer/src/lib.rs +266 -40
- package/crates/vault-transport-unix/src/lib.rs +653 -5
- package/crates/vault-transport-xpc/src/tests.rs +531 -3
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
- package/dist/cli.cjs +663 -190
- package/dist/cli.cjs.map +1 -1
- package/package.json +5 -2
- package/packages/cache/.turbo/turbo-build.log +20 -20
- package/packages/cache/coverage/clover.xml +529 -394
- package/packages/cache/coverage/coverage-final.json +2 -2
- package/packages/cache/coverage/index.html +21 -21
- package/packages/cache/coverage/src/client/index.html +1 -1
- package/packages/cache/coverage/src/client/index.ts.html +1 -1
- package/packages/cache/coverage/src/errors/index.html +1 -1
- package/packages/cache/coverage/src/errors/index.ts.html +12 -12
- package/packages/cache/coverage/src/index.html +1 -1
- package/packages/cache/coverage/src/index.ts.html +1 -1
- package/packages/cache/coverage/src/service/index.html +21 -21
- package/packages/cache/coverage/src/service/index.ts.html +769 -313
- package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
- package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
- package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
- package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
- package/packages/cache/dist/index.cjs +2 -2
- package/packages/cache/dist/index.js +1 -1
- package/packages/cache/dist/service/index.cjs +2 -2
- package/packages/cache/dist/service/index.js +1 -1
- package/packages/cache/node_modules/.bin/tsc +2 -2
- package/packages/cache/node_modules/.bin/tsserver +2 -2
- package/packages/cache/node_modules/.bin/tsup +2 -2
- package/packages/cache/node_modules/.bin/tsup-node +2 -2
- package/packages/cache/node_modules/.bin/vitest +4 -4
- package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/cache/src/service/index.test.ts +165 -19
- package/packages/cache/src/service/index.ts +38 -1
- package/packages/config/.turbo/turbo-build.log +4 -4
- package/packages/config/dist/index.cjs +0 -17
- package/packages/config/dist/index.cjs.map +1 -1
- package/packages/config/src/index.ts +0 -17
- package/packages/rpc/.turbo/turbo-build.log +11 -11
- package/packages/rpc/dist/index.cjs +0 -17
- package/packages/rpc/dist/index.cjs.map +1 -1
- package/packages/rpc/src/index.js +1 -0
- package/packages/ui/node_modules/.bin/tsc +2 -2
- package/packages/ui/node_modules/.bin/tsserver +2 -2
- package/packages/ui/node_modules/.bin/tsup +2 -2
- package/packages/ui/node_modules/.bin/tsup-node +2 -2
- package/scripts/install-cli-launcher.mjs +37 -0
- package/scripts/install-rust-binaries.mjs +47 -0
- package/scripts/run-tests-isolated.mjs +210 -0
- package/src/cli.ts +310 -50
- package/src/lib/admin-reset.ts +15 -30
- package/src/lib/admin-setup.ts +246 -55
- package/src/lib/agent-auth-migrate.ts +5 -1
- package/src/lib/asset-broadcast.ts +15 -4
- package/src/lib/config-amounts.ts +6 -4
- package/src/lib/hidden-tty-prompt.js +1 -0
- package/src/lib/hidden-tty-prompt.ts +105 -0
- package/src/lib/keychain.ts +1 -0
- package/src/lib/local-admin-access.ts +4 -29
- package/src/lib/rust.ts +129 -33
- package/src/lib/signed-tx.ts +1 -0
- package/src/lib/sudo.ts +15 -5
- package/src/lib/wallet-profile.ts +3 -0
- package/src/lib/wallet-setup.ts +52 -0
- package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
- package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
|
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
|
|
|
4
4
|
use std::sync::Arc;
|
|
5
5
|
|
|
6
6
|
use anyhow::{anyhow, bail, Context, Result};
|
|
7
|
+
use async_trait::async_trait;
|
|
7
8
|
use clap::{Parser, ValueEnum};
|
|
8
9
|
use vault_daemon::{DaemonConfig, InMemoryDaemon, PersistentStoreConfig};
|
|
9
10
|
use vault_signer::{SecureEnclaveSignerBackend, SoftwareSignerBackend};
|
|
@@ -120,13 +121,47 @@ impl Drop for StateFileLock {
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
#[async_trait]
|
|
125
|
+
trait DaemonRuntime {
|
|
126
|
+
async fn run_secure_enclave(
|
|
127
|
+
&self,
|
|
128
|
+
daemon_socket: PathBuf,
|
|
129
|
+
allowed_peer_euids: AllowedPeerEuids,
|
|
130
|
+
vault_password: String,
|
|
131
|
+
state_file: PathBuf,
|
|
132
|
+
secure_enclave_label_prefix: String,
|
|
133
|
+
signer_backend_label: &'static str,
|
|
134
|
+
) -> Result<()>;
|
|
135
|
+
|
|
136
|
+
async fn run_software(
|
|
137
|
+
&self,
|
|
138
|
+
daemon_socket: PathBuf,
|
|
139
|
+
allowed_peer_euids: AllowedPeerEuids,
|
|
140
|
+
vault_password: String,
|
|
141
|
+
state_file: PathBuf,
|
|
142
|
+
signer_backend_label: &'static str,
|
|
143
|
+
) -> Result<()>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
struct RealDaemonRuntime;
|
|
147
|
+
|
|
123
148
|
#[tokio::main]
|
|
124
149
|
async fn main() -> Result<()> {
|
|
125
150
|
let cli = Cli::parse();
|
|
126
|
-
validate_signer_backend_runtime(cli.signer_backend)?;
|
|
127
151
|
let mut vault_password = resolve_vault_password(cli.vault_password_stdin, cli.non_interactive)?;
|
|
128
|
-
let
|
|
129
|
-
let
|
|
152
|
+
let runtime = RealDaemonRuntime;
|
|
153
|
+
let result = run_cli_with_runtime(cli, vault_password.clone(), &runtime).await;
|
|
154
|
+
vault_password.zeroize();
|
|
155
|
+
result
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async fn run_cli_with_runtime<R>(cli: Cli, vault_password: String, runtime: &R) -> Result<()>
|
|
159
|
+
where
|
|
160
|
+
R: DaemonRuntime + ?Sized,
|
|
161
|
+
{
|
|
162
|
+
validate_signer_backend_runtime(cli.signer_backend)?;
|
|
163
|
+
let state_file = resolve_state_file_path(cli.state_file.clone())?;
|
|
164
|
+
let daemon_socket = resolve_socket_path(cli.daemon_socket.clone())?;
|
|
130
165
|
let _state_lock = acquire_state_file_lock(&state_file)?;
|
|
131
166
|
|
|
132
167
|
let allowed_peer_euids = resolve_allowed_peer_euids(
|
|
@@ -139,6 +174,71 @@ async fn main() -> Result<()> {
|
|
|
139
174
|
"==> warning: --allow-client-euid grants both admin and agent access; prefer --allow-admin-euid and --allow-agent-euid"
|
|
140
175
|
);
|
|
141
176
|
}
|
|
177
|
+
|
|
178
|
+
dispatch_runtime(
|
|
179
|
+
cli,
|
|
180
|
+
vault_password,
|
|
181
|
+
state_file,
|
|
182
|
+
daemon_socket,
|
|
183
|
+
allowed_peer_euids,
|
|
184
|
+
runtime,
|
|
185
|
+
)
|
|
186
|
+
.await
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async fn dispatch_runtime<R>(
|
|
190
|
+
cli: Cli,
|
|
191
|
+
vault_password: String,
|
|
192
|
+
state_file: PathBuf,
|
|
193
|
+
daemon_socket: PathBuf,
|
|
194
|
+
allowed_peer_euids: AllowedPeerEuids,
|
|
195
|
+
runtime: &R,
|
|
196
|
+
) -> Result<()>
|
|
197
|
+
where
|
|
198
|
+
R: DaemonRuntime + ?Sized,
|
|
199
|
+
{
|
|
200
|
+
let signer_backend_label = relay_signer_backend_label(cli.signer_backend);
|
|
201
|
+
match cli.signer_backend {
|
|
202
|
+
SignerBackendKind::SecureEnclave => {
|
|
203
|
+
runtime
|
|
204
|
+
.run_secure_enclave(
|
|
205
|
+
daemon_socket,
|
|
206
|
+
allowed_peer_euids,
|
|
207
|
+
vault_password,
|
|
208
|
+
state_file,
|
|
209
|
+
cli.secure_enclave_label_prefix,
|
|
210
|
+
signer_backend_label,
|
|
211
|
+
)
|
|
212
|
+
.await
|
|
213
|
+
}
|
|
214
|
+
SignerBackendKind::Software => {
|
|
215
|
+
runtime
|
|
216
|
+
.run_software(
|
|
217
|
+
daemon_socket,
|
|
218
|
+
allowed_peer_euids,
|
|
219
|
+
vault_password,
|
|
220
|
+
state_file,
|
|
221
|
+
signer_backend_label,
|
|
222
|
+
)
|
|
223
|
+
.await
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn print_server_banner(daemon_socket: &Path, allowed_peer_euids: &AllowedPeerEuids) {
|
|
229
|
+
eprintln!(
|
|
230
|
+
"==> daemon listening on {} (allowed admin euid(s): {}; allowed agent euid(s): {})",
|
|
231
|
+
daemon_socket.display(),
|
|
232
|
+
format_allowed_euids(&allowed_peer_euids.admin),
|
|
233
|
+
format_allowed_euids(&allowed_peer_euids.agent)
|
|
234
|
+
);
|
|
235
|
+
eprintln!("==> press Ctrl+C to stop");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async fn bind_server(
|
|
239
|
+
daemon_socket: PathBuf,
|
|
240
|
+
allowed_peer_euids: &AllowedPeerEuids,
|
|
241
|
+
) -> Result<UnixDaemonServer> {
|
|
142
242
|
let server = UnixDaemonServer::bind_with_allowed_peer_euids(
|
|
143
243
|
daemon_socket.clone(),
|
|
144
244
|
allowed_peer_euids.admin.clone(),
|
|
@@ -151,62 +251,70 @@ async fn main() -> Result<()> {
|
|
|
151
251
|
daemon_socket.display()
|
|
152
252
|
)
|
|
153
253
|
})?;
|
|
254
|
+
print_server_banner(&daemon_socket, allowed_peer_euids);
|
|
255
|
+
Ok(server)
|
|
256
|
+
}
|
|
154
257
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
258
|
+
async fn run_bound_daemon<B>(
|
|
259
|
+
server: UnixDaemonServer,
|
|
260
|
+
daemon: Arc<InMemoryDaemon<B>>,
|
|
261
|
+
signer_backend_label: &'static str,
|
|
262
|
+
) -> Result<()>
|
|
263
|
+
where
|
|
264
|
+
B: vault_signer::VaultSignerBackend + Send + Sync + 'static,
|
|
265
|
+
{
|
|
266
|
+
let relay_task = relay_sync::spawn_relay_sync_task(Arc::clone(&daemon), signer_backend_label);
|
|
267
|
+
server
|
|
268
|
+
.run_until_shutdown(daemon, async {
|
|
269
|
+
let _ = tokio::signal::ctrl_c().await;
|
|
270
|
+
relay_task.abort();
|
|
271
|
+
})
|
|
272
|
+
.await
|
|
273
|
+
.context("daemon server loop failed")
|
|
274
|
+
}
|
|
162
275
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
.context("daemon server loop failed")?;
|
|
185
|
-
}
|
|
186
|
-
SignerBackendKind::Software => {
|
|
187
|
-
let daemon = Arc::new(
|
|
188
|
-
InMemoryDaemon::new_with_persistent_store(
|
|
189
|
-
&vault_password,
|
|
190
|
-
SoftwareSignerBackend::default(),
|
|
191
|
-
DaemonConfig::default(),
|
|
192
|
-
PersistentStoreConfig::new(state_file),
|
|
193
|
-
)
|
|
194
|
-
.context("failed to initialize daemon")?,
|
|
195
|
-
);
|
|
196
|
-
let relay_task =
|
|
197
|
-
relay_sync::spawn_relay_sync_task(Arc::clone(&daemon), signer_backend_label);
|
|
198
|
-
vault_password.zeroize();
|
|
199
|
-
server
|
|
200
|
-
.run_until_shutdown(daemon, async {
|
|
201
|
-
let _ = tokio::signal::ctrl_c().await;
|
|
202
|
-
relay_task.abort();
|
|
203
|
-
})
|
|
204
|
-
.await
|
|
205
|
-
.context("daemon server loop failed")?;
|
|
206
|
-
}
|
|
276
|
+
#[async_trait]
|
|
277
|
+
impl DaemonRuntime for RealDaemonRuntime {
|
|
278
|
+
async fn run_secure_enclave(
|
|
279
|
+
&self,
|
|
280
|
+
daemon_socket: PathBuf,
|
|
281
|
+
allowed_peer_euids: AllowedPeerEuids,
|
|
282
|
+
mut vault_password: String,
|
|
283
|
+
state_file: PathBuf,
|
|
284
|
+
secure_enclave_label_prefix: String,
|
|
285
|
+
signer_backend_label: &'static str,
|
|
286
|
+
) -> Result<()> {
|
|
287
|
+
let server = bind_server(daemon_socket, &allowed_peer_euids).await?;
|
|
288
|
+
let daemon = InMemoryDaemon::new_with_persistent_store(
|
|
289
|
+
&vault_password,
|
|
290
|
+
SecureEnclaveSignerBackend::new(secure_enclave_label_prefix),
|
|
291
|
+
DaemonConfig::default(),
|
|
292
|
+
PersistentStoreConfig::new(state_file),
|
|
293
|
+
);
|
|
294
|
+
vault_password.zeroize();
|
|
295
|
+
let daemon = Arc::new(daemon.context("failed to initialize daemon")?);
|
|
296
|
+
run_bound_daemon(server, daemon, signer_backend_label).await
|
|
207
297
|
}
|
|
208
298
|
|
|
209
|
-
|
|
299
|
+
async fn run_software(
|
|
300
|
+
&self,
|
|
301
|
+
daemon_socket: PathBuf,
|
|
302
|
+
allowed_peer_euids: AllowedPeerEuids,
|
|
303
|
+
mut vault_password: String,
|
|
304
|
+
state_file: PathBuf,
|
|
305
|
+
signer_backend_label: &'static str,
|
|
306
|
+
) -> Result<()> {
|
|
307
|
+
let server = bind_server(daemon_socket, &allowed_peer_euids).await?;
|
|
308
|
+
let daemon = InMemoryDaemon::new_with_persistent_store(
|
|
309
|
+
&vault_password,
|
|
310
|
+
SoftwareSignerBackend::default(),
|
|
311
|
+
DaemonConfig::default(),
|
|
312
|
+
PersistentStoreConfig::new(state_file),
|
|
313
|
+
);
|
|
314
|
+
vault_password.zeroize();
|
|
315
|
+
let daemon = Arc::new(daemon.context("failed to initialize daemon")?);
|
|
316
|
+
run_bound_daemon(server, daemon, signer_backend_label).await
|
|
317
|
+
}
|
|
210
318
|
}
|
|
211
319
|
|
|
212
320
|
fn relay_signer_backend_label(backend: SignerBackendKind) -> &'static str {
|
|
@@ -514,14 +622,148 @@ fn lock_path(path: &Path) -> PathBuf {
|
|
|
514
622
|
#[cfg(test)]
|
|
515
623
|
mod tests {
|
|
516
624
|
use super::{
|
|
517
|
-
|
|
518
|
-
|
|
625
|
+
acquire_state_file_lock, default_socket_path, default_state_file_path, dispatch_runtime,
|
|
626
|
+
ensure_file_parent, format_allowed_euids, is_symlink, lock_path, read_secret_from_reader,
|
|
627
|
+
relay_signer_backend_label, resolve_allowed_peer_euids, resolve_allowed_peer_euids_with_sudo_uid,
|
|
628
|
+
resolve_socket_path, resolve_state_file_path, resolve_vault_password, run_cli_with_runtime,
|
|
629
|
+
validate_password, validate_signer_backend_runtime, wlfi_home_dir, AllowedPeerEuids, Cli,
|
|
630
|
+
DaemonRuntime, SignerBackendKind,
|
|
519
631
|
};
|
|
632
|
+
use anyhow::{anyhow, Result};
|
|
633
|
+
use async_trait::async_trait;
|
|
520
634
|
use clap::Parser;
|
|
521
635
|
use std::collections::BTreeSet;
|
|
522
|
-
use std::
|
|
636
|
+
use std::io::{Cursor, Read};
|
|
637
|
+
use std::path::{Path, PathBuf};
|
|
638
|
+
use std::sync::{Mutex, OnceLock};
|
|
523
639
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
524
640
|
|
|
641
|
+
fn env_lock() -> &'static Mutex<()> {
|
|
642
|
+
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
|
643
|
+
ENV_LOCK.get_or_init(|| Mutex::new(()))
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
fn temp_path(prefix: &str) -> PathBuf {
|
|
647
|
+
std::env::temp_dir().join(format!(
|
|
648
|
+
"{prefix}-{}-{}",
|
|
649
|
+
std::process::id(),
|
|
650
|
+
SystemTime::now()
|
|
651
|
+
.duration_since(UNIX_EPOCH)
|
|
652
|
+
.expect("time")
|
|
653
|
+
.as_nanos()
|
|
654
|
+
))
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
struct FailingReader;
|
|
658
|
+
|
|
659
|
+
impl Read for FailingReader {
|
|
660
|
+
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
|
|
661
|
+
Err(std::io::Error::other("boom"))
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
666
|
+
enum RuntimeCall {
|
|
667
|
+
SecureEnclave {
|
|
668
|
+
daemon_socket: PathBuf,
|
|
669
|
+
allowed_admin: BTreeSet<u32>,
|
|
670
|
+
allowed_agent: BTreeSet<u32>,
|
|
671
|
+
vault_password: String,
|
|
672
|
+
state_file: PathBuf,
|
|
673
|
+
secure_enclave_label_prefix: String,
|
|
674
|
+
signer_backend_label: &'static str,
|
|
675
|
+
},
|
|
676
|
+
Software {
|
|
677
|
+
daemon_socket: PathBuf,
|
|
678
|
+
allowed_admin: BTreeSet<u32>,
|
|
679
|
+
allowed_agent: BTreeSet<u32>,
|
|
680
|
+
vault_password: String,
|
|
681
|
+
state_file: PathBuf,
|
|
682
|
+
signer_backend_label: &'static str,
|
|
683
|
+
},
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
struct FakeRuntime {
|
|
687
|
+
calls: Mutex<Vec<RuntimeCall>>,
|
|
688
|
+
fail_message: Option<&'static str>,
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
#[async_trait]
|
|
692
|
+
impl DaemonRuntime for FakeRuntime {
|
|
693
|
+
async fn run_secure_enclave(
|
|
694
|
+
&self,
|
|
695
|
+
daemon_socket: PathBuf,
|
|
696
|
+
allowed_peer_euids: AllowedPeerEuids,
|
|
697
|
+
vault_password: String,
|
|
698
|
+
state_file: PathBuf,
|
|
699
|
+
secure_enclave_label_prefix: String,
|
|
700
|
+
signer_backend_label: &'static str,
|
|
701
|
+
) -> Result<()> {
|
|
702
|
+
self.calls
|
|
703
|
+
.lock()
|
|
704
|
+
.expect("lock")
|
|
705
|
+
.push(RuntimeCall::SecureEnclave {
|
|
706
|
+
daemon_socket,
|
|
707
|
+
allowed_admin: allowed_peer_euids.admin,
|
|
708
|
+
allowed_agent: allowed_peer_euids.agent,
|
|
709
|
+
vault_password,
|
|
710
|
+
state_file,
|
|
711
|
+
secure_enclave_label_prefix,
|
|
712
|
+
signer_backend_label,
|
|
713
|
+
});
|
|
714
|
+
match self.fail_message {
|
|
715
|
+
Some(message) => Err(anyhow!(message)),
|
|
716
|
+
None => Ok(()),
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async fn run_software(
|
|
721
|
+
&self,
|
|
722
|
+
daemon_socket: PathBuf,
|
|
723
|
+
allowed_peer_euids: AllowedPeerEuids,
|
|
724
|
+
vault_password: String,
|
|
725
|
+
state_file: PathBuf,
|
|
726
|
+
signer_backend_label: &'static str,
|
|
727
|
+
) -> Result<()> {
|
|
728
|
+
self.calls
|
|
729
|
+
.lock()
|
|
730
|
+
.expect("lock")
|
|
731
|
+
.push(RuntimeCall::Software {
|
|
732
|
+
daemon_socket,
|
|
733
|
+
allowed_admin: allowed_peer_euids.admin,
|
|
734
|
+
allowed_agent: allowed_peer_euids.agent,
|
|
735
|
+
vault_password,
|
|
736
|
+
state_file,
|
|
737
|
+
signer_backend_label,
|
|
738
|
+
});
|
|
739
|
+
match self.fail_message {
|
|
740
|
+
Some(message) => Err(anyhow!(message)),
|
|
741
|
+
None => Ok(()),
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
fn sample_cli(root: &Path, signer_backend: SignerBackendKind) -> Cli {
|
|
747
|
+
Cli {
|
|
748
|
+
vault_password_stdin: false,
|
|
749
|
+
non_interactive: false,
|
|
750
|
+
state_file: Some(root.join("state").join("daemon-state.enc")),
|
|
751
|
+
daemon_socket: Some(root.join("socket").join("daemon.sock")),
|
|
752
|
+
secure_enclave_label_prefix: "com.wlfi.test".to_string(),
|
|
753
|
+
signer_backend,
|
|
754
|
+
allow_admin_euid: vec![11],
|
|
755
|
+
allow_agent_euid: vec![22],
|
|
756
|
+
allow_client_euid: vec![33],
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
fn test_runtime() -> tokio::runtime::Runtime {
|
|
761
|
+
tokio::runtime::Builder::new_current_thread()
|
|
762
|
+
.enable_all()
|
|
763
|
+
.build()
|
|
764
|
+
.expect("runtime")
|
|
765
|
+
}
|
|
766
|
+
|
|
525
767
|
#[test]
|
|
526
768
|
fn validate_password_rejects_oversized_non_stdin_secret() {
|
|
527
769
|
let err = validate_password("a".repeat((16 * 1024) + 1), "argument or environment")
|
|
@@ -529,6 +771,12 @@ mod tests {
|
|
|
529
771
|
assert!(err.to_string().contains("must not exceed"));
|
|
530
772
|
}
|
|
531
773
|
|
|
774
|
+
#[test]
|
|
775
|
+
fn validate_password_rejects_whitespace_only() {
|
|
776
|
+
let err = validate_password(" \n\t ".to_string(), "stdin").expect_err("must fail");
|
|
777
|
+
assert!(err.to_string().contains("must not be empty or whitespace"));
|
|
778
|
+
}
|
|
779
|
+
|
|
532
780
|
#[test]
|
|
533
781
|
fn cli_rejects_inline_vault_password_argument() {
|
|
534
782
|
let err = Cli::try_parse_from([
|
|
@@ -547,18 +795,30 @@ mod tests {
|
|
|
547
795
|
assert!(err.to_string().contains("use --vault-password-stdin"));
|
|
548
796
|
}
|
|
549
797
|
|
|
798
|
+
#[test]
|
|
799
|
+
fn read_secret_from_reader_trims_trailing_newlines() {
|
|
800
|
+
let secret = read_secret_from_reader(Cursor::new("vault-secret\r\n"), "vault password")
|
|
801
|
+
.expect("trimmed secret");
|
|
802
|
+
assert_eq!(secret, "vault-secret");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
#[test]
|
|
806
|
+
fn read_secret_from_reader_rejects_blank_after_trimming() {
|
|
807
|
+
let err = read_secret_from_reader(Cursor::new(" \n"), "vault password")
|
|
808
|
+
.expect_err("must fail");
|
|
809
|
+
assert!(err.to_string().contains("must not be empty or whitespace"));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
#[test]
|
|
813
|
+
fn read_secret_from_reader_propagates_io_errors() {
|
|
814
|
+
let err = read_secret_from_reader(FailingReader, "vault password").expect_err("must fail");
|
|
815
|
+
assert!(err.to_string().contains("failed to read vault password from stdin"));
|
|
816
|
+
}
|
|
817
|
+
|
|
550
818
|
#[cfg(unix)]
|
|
551
819
|
#[test]
|
|
552
820
|
fn ensure_file_parent_accepts_current_user_owned_directory() {
|
|
553
|
-
let
|
|
554
|
-
.duration_since(UNIX_EPOCH)
|
|
555
|
-
.expect("time")
|
|
556
|
-
.as_nanos();
|
|
557
|
-
let parent = std::env::temp_dir().join(format!(
|
|
558
|
-
"wlfi-daemon-parent-{}-{}",
|
|
559
|
-
std::process::id(),
|
|
560
|
-
unique
|
|
561
|
-
));
|
|
821
|
+
let parent = temp_path("wlfi-daemon-parent");
|
|
562
822
|
std::fs::create_dir_all(&parent).expect("create parent");
|
|
563
823
|
let path = parent.join("daemon-state.enc");
|
|
564
824
|
ensure_file_parent(&path, "state").expect("current-user-owned directory should pass");
|
|
@@ -616,6 +876,13 @@ mod tests {
|
|
|
616
876
|
assert_eq!(resolved.agent, BTreeSet::from([0, 22, 33]));
|
|
617
877
|
}
|
|
618
878
|
|
|
879
|
+
#[test]
|
|
880
|
+
fn resolve_allowed_peer_euids_wrapper_matches_internal_helper() {
|
|
881
|
+
let resolved = resolve_allowed_peer_euids(&[7], &[8], &[9]).expect("allowed peer euids");
|
|
882
|
+
assert_eq!(resolved.admin, BTreeSet::from([0, 7, 9]));
|
|
883
|
+
assert_eq!(resolved.agent, BTreeSet::from([0, 8, 9]));
|
|
884
|
+
}
|
|
885
|
+
|
|
619
886
|
#[test]
|
|
620
887
|
fn relay_signer_backend_label_matches_runtime_backend() {
|
|
621
888
|
assert_eq!(
|
|
@@ -628,6 +895,289 @@ mod tests {
|
|
|
628
895
|
);
|
|
629
896
|
}
|
|
630
897
|
|
|
898
|
+
#[test]
|
|
899
|
+
fn validate_signer_backend_runtime_accepts_software_everywhere() {
|
|
900
|
+
validate_signer_backend_runtime(SignerBackendKind::Software).expect("software backend");
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
#[cfg(not(target_os = "macos"))]
|
|
904
|
+
#[test]
|
|
905
|
+
fn validate_signer_backend_runtime_rejects_secure_enclave_off_macos() {
|
|
906
|
+
let err = validate_signer_backend_runtime(SignerBackendKind::SecureEnclave)
|
|
907
|
+
.expect_err("must fail");
|
|
908
|
+
assert!(err
|
|
909
|
+
.to_string()
|
|
910
|
+
.contains("Secure Enclave daemon mode is supported only on macOS"));
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
#[test]
|
|
914
|
+
fn format_allowed_euids_renders_sorted_csv() {
|
|
915
|
+
assert_eq!(format_allowed_euids(&BTreeSet::from([0, 11, 33])), "0,11,33");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
#[test]
|
|
919
|
+
fn lock_path_appends_lock_suffix() {
|
|
920
|
+
assert_eq!(
|
|
921
|
+
lock_path(Path::new("/tmp/wlfi/daemon-state.enc")),
|
|
922
|
+
PathBuf::from("/tmp/wlfi/daemon-state.enc.lock")
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
#[test]
|
|
927
|
+
fn wlfi_home_dir_prefers_wlfi_home_and_falls_back_to_home() {
|
|
928
|
+
let _guard = env_lock().lock().expect("env lock");
|
|
929
|
+
let wlfi_home = temp_path("wlfi-home");
|
|
930
|
+
let home = temp_path("home-dir");
|
|
931
|
+
|
|
932
|
+
std::env::set_var("WLFI_HOME", &wlfi_home);
|
|
933
|
+
std::env::set_var("HOME", &home);
|
|
934
|
+
assert_eq!(wlfi_home_dir().expect("wlfi home"), wlfi_home);
|
|
935
|
+
|
|
936
|
+
std::env::remove_var("WLFI_HOME");
|
|
937
|
+
assert_eq!(
|
|
938
|
+
wlfi_home_dir().expect("home fallback"),
|
|
939
|
+
home.join(".wlfi_agent")
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
std::env::remove_var("HOME");
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
#[test]
|
|
946
|
+
fn wlfi_home_dir_rejects_empty_and_missing_env() {
|
|
947
|
+
let _guard = env_lock().lock().expect("env lock");
|
|
948
|
+
|
|
949
|
+
std::env::set_var("WLFI_HOME", "");
|
|
950
|
+
let err = wlfi_home_dir().expect_err("must reject empty WLFI_HOME");
|
|
951
|
+
assert!(err.to_string().contains("WLFI_HOME must not be empty"));
|
|
952
|
+
|
|
953
|
+
std::env::remove_var("WLFI_HOME");
|
|
954
|
+
std::env::remove_var("HOME");
|
|
955
|
+
let err = wlfi_home_dir().expect_err("must reject missing HOME");
|
|
956
|
+
assert!(err
|
|
957
|
+
.to_string()
|
|
958
|
+
.contains("HOME is not set; use WLFI_HOME to choose config directory"));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
#[test]
|
|
962
|
+
fn default_paths_and_resolvers_use_wlfi_home() {
|
|
963
|
+
let _guard = env_lock().lock().expect("env lock");
|
|
964
|
+
let wlfi_home = temp_path("wlfi-daemon-defaults");
|
|
965
|
+
std::env::set_var("WLFI_HOME", &wlfi_home);
|
|
966
|
+
|
|
967
|
+
assert_eq!(
|
|
968
|
+
default_state_file_path().expect("default state"),
|
|
969
|
+
wlfi_home.join("daemon-state.enc")
|
|
970
|
+
);
|
|
971
|
+
assert_eq!(
|
|
972
|
+
default_socket_path().expect("default socket"),
|
|
973
|
+
wlfi_home.join("daemon.sock")
|
|
974
|
+
);
|
|
975
|
+
assert_eq!(
|
|
976
|
+
resolve_state_file_path(None).expect("resolved state"),
|
|
977
|
+
wlfi_home.join("daemon-state.enc")
|
|
978
|
+
);
|
|
979
|
+
assert_eq!(
|
|
980
|
+
resolve_socket_path(None).expect("resolved socket"),
|
|
981
|
+
wlfi_home.join("daemon.sock")
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
std::env::remove_var("WLFI_HOME");
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
#[test]
|
|
988
|
+
fn dispatch_runtime_routes_to_expected_backend() {
|
|
989
|
+
let runtime = test_runtime();
|
|
990
|
+
let root = temp_path("wlfi-daemon-dispatch");
|
|
991
|
+
let state_file = root.join("state.enc");
|
|
992
|
+
let daemon_socket = root.join("daemon.sock");
|
|
993
|
+
let allowed = AllowedPeerEuids {
|
|
994
|
+
admin: BTreeSet::from([0, 11, 33]),
|
|
995
|
+
agent: BTreeSet::from([0, 22, 33]),
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
let secure_runtime = FakeRuntime {
|
|
999
|
+
calls: Mutex::new(Vec::new()),
|
|
1000
|
+
fail_message: None,
|
|
1001
|
+
};
|
|
1002
|
+
runtime
|
|
1003
|
+
.block_on(dispatch_runtime(
|
|
1004
|
+
sample_cli(&root, SignerBackendKind::SecureEnclave),
|
|
1005
|
+
"vault-secret".to_string(),
|
|
1006
|
+
state_file.clone(),
|
|
1007
|
+
daemon_socket.clone(),
|
|
1008
|
+
allowed.clone(),
|
|
1009
|
+
&secure_runtime,
|
|
1010
|
+
))
|
|
1011
|
+
.expect("secure enclave dispatch");
|
|
1012
|
+
assert_eq!(
|
|
1013
|
+
secure_runtime.calls.lock().expect("lock").as_slice(),
|
|
1014
|
+
&[RuntimeCall::SecureEnclave {
|
|
1015
|
+
daemon_socket: daemon_socket.clone(),
|
|
1016
|
+
allowed_admin: BTreeSet::from([0, 11, 33]),
|
|
1017
|
+
allowed_agent: BTreeSet::from([0, 22, 33]),
|
|
1018
|
+
vault_password: "vault-secret".to_string(),
|
|
1019
|
+
state_file: state_file.clone(),
|
|
1020
|
+
secure_enclave_label_prefix: "com.wlfi.test".to_string(),
|
|
1021
|
+
signer_backend_label: "secure-enclave",
|
|
1022
|
+
}]
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
let software_runtime = FakeRuntime {
|
|
1026
|
+
calls: Mutex::new(Vec::new()),
|
|
1027
|
+
fail_message: None,
|
|
1028
|
+
};
|
|
1029
|
+
runtime
|
|
1030
|
+
.block_on(dispatch_runtime(
|
|
1031
|
+
sample_cli(&root, SignerBackendKind::Software),
|
|
1032
|
+
"vault-secret".to_string(),
|
|
1033
|
+
state_file.clone(),
|
|
1034
|
+
daemon_socket.clone(),
|
|
1035
|
+
allowed,
|
|
1036
|
+
&software_runtime,
|
|
1037
|
+
))
|
|
1038
|
+
.expect("software dispatch");
|
|
1039
|
+
assert_eq!(
|
|
1040
|
+
software_runtime.calls.lock().expect("lock").as_slice(),
|
|
1041
|
+
&[RuntimeCall::Software {
|
|
1042
|
+
daemon_socket,
|
|
1043
|
+
allowed_admin: BTreeSet::from([0, 11, 33]),
|
|
1044
|
+
allowed_agent: BTreeSet::from([0, 22, 33]),
|
|
1045
|
+
vault_password: "vault-secret".to_string(),
|
|
1046
|
+
state_file,
|
|
1047
|
+
signer_backend_label: "software",
|
|
1048
|
+
}]
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
#[test]
|
|
1053
|
+
fn run_cli_with_runtime_resolves_paths_and_lock_before_invoking_runtime() {
|
|
1054
|
+
let runtime = test_runtime();
|
|
1055
|
+
let root = temp_path("wlfi-daemon-run-runtime");
|
|
1056
|
+
let cli = sample_cli(&root, SignerBackendKind::Software);
|
|
1057
|
+
let fake_runtime = FakeRuntime {
|
|
1058
|
+
calls: Mutex::new(Vec::new()),
|
|
1059
|
+
fail_message: None,
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
runtime
|
|
1063
|
+
.block_on(run_cli_with_runtime(
|
|
1064
|
+
cli,
|
|
1065
|
+
"vault-secret".to_string(),
|
|
1066
|
+
&fake_runtime,
|
|
1067
|
+
))
|
|
1068
|
+
.expect("runtime dispatch");
|
|
1069
|
+
|
|
1070
|
+
assert!(root.join("state").exists());
|
|
1071
|
+
assert!(root.join("socket").exists());
|
|
1072
|
+
assert!(root.join("state").join("daemon-state.enc.lock").exists());
|
|
1073
|
+
assert_eq!(fake_runtime.calls.lock().expect("lock").len(), 1);
|
|
1074
|
+
|
|
1075
|
+
std::fs::remove_dir_all(&root).expect("cleanup");
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
#[cfg(not(target_os = "macos"))]
|
|
1079
|
+
#[test]
|
|
1080
|
+
fn run_cli_with_runtime_rejects_secure_enclave_before_runtime_invocation() {
|
|
1081
|
+
let runtime = test_runtime();
|
|
1082
|
+
let root = temp_path("wlfi-daemon-secure-enclave");
|
|
1083
|
+
let fake_runtime = FakeRuntime {
|
|
1084
|
+
calls: Mutex::new(Vec::new()),
|
|
1085
|
+
fail_message: None,
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
let err = runtime
|
|
1089
|
+
.block_on(run_cli_with_runtime(
|
|
1090
|
+
sample_cli(&root, SignerBackendKind::SecureEnclave),
|
|
1091
|
+
"vault-secret".to_string(),
|
|
1092
|
+
&fake_runtime,
|
|
1093
|
+
))
|
|
1094
|
+
.expect_err("secure enclave must fail");
|
|
1095
|
+
assert!(err
|
|
1096
|
+
.to_string()
|
|
1097
|
+
.contains("Secure Enclave daemon mode is supported only on macOS"));
|
|
1098
|
+
assert!(fake_runtime.calls.lock().expect("lock").is_empty());
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
#[test]
|
|
1102
|
+
fn dispatch_runtime_bubbles_runtime_failures() {
|
|
1103
|
+
let runtime = test_runtime();
|
|
1104
|
+
let root = temp_path("wlfi-daemon-runtime-error");
|
|
1105
|
+
let err_runtime = FakeRuntime {
|
|
1106
|
+
calls: Mutex::new(Vec::new()),
|
|
1107
|
+
fail_message: Some("runtime boom"),
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
let err = runtime
|
|
1111
|
+
.block_on(dispatch_runtime(
|
|
1112
|
+
sample_cli(&root, SignerBackendKind::Software),
|
|
1113
|
+
"vault-secret".to_string(),
|
|
1114
|
+
root.join("daemon-state.enc"),
|
|
1115
|
+
root.join("daemon.sock"),
|
|
1116
|
+
AllowedPeerEuids {
|
|
1117
|
+
admin: BTreeSet::from([0]),
|
|
1118
|
+
agent: BTreeSet::from([0]),
|
|
1119
|
+
},
|
|
1120
|
+
&err_runtime,
|
|
1121
|
+
))
|
|
1122
|
+
.expect_err("runtime failure must bubble");
|
|
1123
|
+
assert!(err.to_string().contains("runtime boom"));
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
#[test]
|
|
1127
|
+
fn explicit_state_and_socket_paths_are_preserved() {
|
|
1128
|
+
let root = temp_path("wlfi-daemon-explicit");
|
|
1129
|
+
let state = root.join("state").join("daemon-state.enc");
|
|
1130
|
+
let socket = root.join("sock").join("daemon.sock");
|
|
1131
|
+
|
|
1132
|
+
let resolved_state = resolve_state_file_path(Some(state.clone())).expect("state path");
|
|
1133
|
+
let resolved_socket = resolve_socket_path(Some(socket.clone())).expect("socket path");
|
|
1134
|
+
assert_eq!(resolved_state, state);
|
|
1135
|
+
assert_eq!(resolved_socket, socket);
|
|
1136
|
+
assert!(root.join("state").exists());
|
|
1137
|
+
assert!(root.join("sock").exists());
|
|
1138
|
+
|
|
1139
|
+
std::fs::remove_dir_all(&root).expect("cleanup");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
#[cfg(unix)]
|
|
1143
|
+
#[test]
|
|
1144
|
+
fn acquire_state_file_lock_creates_lock_file() {
|
|
1145
|
+
let root = temp_path("wlfi-daemon-lock");
|
|
1146
|
+
let state_path = root.join("daemon-state.enc");
|
|
1147
|
+
|
|
1148
|
+
let lock = acquire_state_file_lock(&state_path).expect("lock file");
|
|
1149
|
+
let lock_file = lock_path(&state_path);
|
|
1150
|
+
assert!(lock_file.exists());
|
|
1151
|
+
drop(lock);
|
|
1152
|
+
|
|
1153
|
+
std::fs::remove_dir_all(&root).expect("cleanup");
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
#[cfg(unix)]
|
|
1157
|
+
#[test]
|
|
1158
|
+
fn ensure_file_parent_rejects_symlink_path_and_is_symlink_reports_it() {
|
|
1159
|
+
use std::os::unix::fs::symlink;
|
|
1160
|
+
|
|
1161
|
+
let root = temp_path("wlfi-daemon-symlink");
|
|
1162
|
+
std::fs::create_dir_all(&root).expect("create root");
|
|
1163
|
+
let target = root.join("real-state.enc");
|
|
1164
|
+
let link = root.join("linked-state.enc");
|
|
1165
|
+
std::fs::write(&target, "seed").expect("seed");
|
|
1166
|
+
symlink(&target, &link).expect("symlink");
|
|
1167
|
+
|
|
1168
|
+
assert!(is_symlink(&link).expect("symlink metadata"));
|
|
1169
|
+
let err = ensure_file_parent(&link, "state").expect_err("must reject symlink file");
|
|
1170
|
+
assert!(err.to_string().contains("must not be a symlink"));
|
|
1171
|
+
|
|
1172
|
+
std::fs::remove_dir_all(&root).expect("cleanup");
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
#[test]
|
|
1176
|
+
fn is_symlink_returns_false_for_missing_path() {
|
|
1177
|
+
let missing = temp_path("wlfi-daemon-missing");
|
|
1178
|
+
assert!(!is_symlink(&missing).expect("missing path is not symlink"));
|
|
1179
|
+
}
|
|
1180
|
+
|
|
631
1181
|
#[cfg(unix)]
|
|
632
1182
|
#[test]
|
|
633
1183
|
fn assert_allowed_directory_owner_rejects_non_root_owner_for_root_runtime() {
|