@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.
- package/Cargo.lock +3968 -0
- package/Cargo.toml +50 -0
- package/README.md +426 -6
- package/crates/vault-cli-admin/Cargo.toml +26 -0
- package/crates/vault-cli-admin/src/io_utils.rs +500 -0
- package/crates/vault-cli-admin/src/main.rs +3990 -0
- package/crates/vault-cli-admin/src/shared_config.rs +624 -0
- package/crates/vault-cli-admin/src/tui/amounts.rs +180 -0
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +250 -0
- package/crates/vault-cli-admin/src/tui/utils.rs +82 -0
- package/crates/vault-cli-admin/src/tui.rs +3410 -0
- package/crates/vault-cli-agent/Cargo.toml +24 -0
- package/crates/vault-cli-agent/src/io_utils.rs +576 -0
- package/crates/vault-cli-agent/src/main.rs +833 -0
- package/crates/vault-cli-daemon/Cargo.toml +28 -0
- package/crates/vault-cli-daemon/src/bin/wlfi-agent-system-keychain.rs +216 -0
- package/crates/vault-cli-daemon/src/main.rs +644 -0
- package/crates/vault-cli-daemon/src/relay_sync.rs +894 -0
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +167 -0
- package/crates/vault-daemon/Cargo.toml +32 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +1041 -0
- package/crates/vault-daemon/src/daemon_parts/core_helpers.rs +1256 -0
- package/crates/vault-daemon/src/daemon_parts/types_api_rpc.rs +622 -0
- package/crates/vault-daemon/src/lib.rs +54 -0
- package/crates/vault-daemon/src/persistence.rs +441 -0
- package/crates/vault-daemon/src/tests.rs +237 -0
- package/crates/vault-daemon/src/tests_parts/part1.rs +1224 -0
- package/crates/vault-daemon/src/tests_parts/part2.rs +1021 -0
- package/crates/vault-daemon/src/tests_parts/part3.rs +835 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +604 -0
- package/crates/vault-domain/Cargo.toml +20 -0
- package/crates/vault-domain/src/action.rs +849 -0
- package/crates/vault-domain/src/address.rs +51 -0
- package/crates/vault-domain/src/approval.rs +90 -0
- package/crates/vault-domain/src/constants.rs +4 -0
- package/crates/vault-domain/src/error.rs +54 -0
- package/crates/vault-domain/src/keys.rs +71 -0
- package/crates/vault-domain/src/lib.rs +42 -0
- package/crates/vault-domain/src/nonce.rs +102 -0
- package/crates/vault-domain/src/policy.rs +172 -0
- package/crates/vault-domain/src/request.rs +53 -0
- package/crates/vault-domain/src/scope.rs +24 -0
- package/crates/vault-domain/src/session.rs +50 -0
- package/crates/vault-domain/src/signature.rs +34 -0
- package/crates/vault-domain/src/tests.rs +651 -0
- package/crates/vault-domain/src/u128_as_decimal_string.rs +44 -0
- package/crates/vault-policy/Cargo.toml +17 -0
- package/crates/vault-policy/src/engine.rs +301 -0
- package/crates/vault-policy/src/error.rs +81 -0
- package/crates/vault-policy/src/lib.rs +17 -0
- package/crates/vault-policy/src/report.rs +34 -0
- package/crates/vault-policy/src/tests.rs +891 -0
- package/crates/vault-policy/src/tests_explain.rs +78 -0
- package/crates/vault-sdk-agent/Cargo.toml +21 -0
- package/crates/vault-sdk-agent/src/lib.rs +711 -0
- package/crates/vault-signer/Cargo.toml +25 -0
- package/crates/vault-signer/src/lib.rs +731 -0
- package/crates/vault-signer/tests/secure_enclave_acl.rs +54 -0
- package/crates/vault-transport-unix/Cargo.toml +24 -0
- package/crates/vault-transport-unix/src/lib.rs +1640 -0
- package/crates/vault-transport-xpc/Cargo.toml +25 -0
- package/crates/vault-transport-xpc/src/client_codec_api.rs +635 -0
- package/crates/vault-transport-xpc/src/lib.rs +680 -0
- package/crates/vault-transport-xpc/src/tests.rs +818 -0
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +773 -0
- package/dist/cli.cjs +35088 -0
- package/dist/cli.cjs.map +1 -0
- package/package.json +45 -41
- package/packages/cache/.turbo/turbo-build.log +52 -0
- package/packages/cache/dist/chunk-2QFWMUXT.cjs +43 -0
- package/packages/cache/dist/chunk-2QFWMUXT.cjs.map +1 -0
- package/packages/cache/dist/chunk-4U63TZTQ.js +43 -0
- package/packages/cache/dist/chunk-4U63TZTQ.js.map +1 -0
- package/packages/cache/dist/chunk-ALQ6H7KG.cjs +404 -0
- package/packages/cache/dist/chunk-ALQ6H7KG.cjs.map +1 -0
- package/packages/cache/dist/chunk-FGJEEF5N.js +404 -0
- package/packages/cache/dist/chunk-FGJEEF5N.js.map +1 -0
- package/packages/cache/dist/chunk-UYNEHZHB.cjs +45 -0
- package/packages/cache/dist/chunk-UYNEHZHB.cjs.map +1 -0
- package/packages/cache/dist/chunk-VXVMPG3W.js +45 -0
- package/packages/cache/dist/chunk-VXVMPG3W.js.map +1 -0
- package/packages/cache/dist/client/index.cjs +11 -0
- package/packages/cache/dist/client/index.cjs.map +1 -0
- package/packages/cache/dist/client/index.d.cts +15 -0
- package/packages/cache/dist/client/index.d.ts +15 -0
- package/packages/cache/dist/client/index.js +11 -0
- package/packages/cache/dist/client/index.js.map +1 -0
- package/packages/cache/dist/errors/index.cjs +11 -0
- package/packages/cache/dist/errors/index.cjs.map +1 -0
- package/packages/cache/dist/errors/index.d.cts +26 -0
- package/packages/cache/dist/errors/index.d.ts +26 -0
- package/packages/cache/dist/errors/index.js +11 -0
- package/packages/cache/dist/errors/index.js.map +1 -0
- package/packages/cache/dist/index.cjs +29 -0
- package/packages/cache/dist/index.cjs.map +1 -0
- package/packages/cache/dist/index.d.cts +4 -0
- package/packages/cache/dist/index.d.ts +4 -0
- package/packages/cache/dist/index.js +29 -0
- package/packages/cache/dist/index.js.map +1 -0
- package/packages/cache/dist/service/index.cjs +15 -0
- package/packages/cache/dist/service/index.cjs.map +1 -0
- package/packages/cache/dist/service/index.d.cts +184 -0
- package/packages/cache/dist/service/index.d.ts +184 -0
- package/packages/cache/dist/service/index.js +15 -0
- package/packages/cache/dist/service/index.js.map +1 -0
- package/packages/cache/node_modules/.bin/jiti +17 -0
- package/packages/cache/node_modules/.bin/tsc +17 -0
- package/packages/cache/node_modules/.bin/tsserver +17 -0
- package/packages/cache/node_modules/.bin/tsup +17 -0
- package/packages/cache/node_modules/.bin/tsup-node +17 -0
- package/packages/cache/node_modules/.bin/tsx +17 -0
- package/packages/cache/node_modules/.bin/vitest +17 -0
- package/packages/cache/package.json +48 -0
- package/packages/cache/src/client/index.ts +56 -0
- package/packages/cache/src/errors/index.ts +53 -0
- package/packages/cache/src/index.ts +3 -0
- package/packages/cache/src/service/index.test.ts +263 -0
- package/packages/cache/src/service/index.ts +678 -0
- package/packages/cache/tsconfig.json +13 -0
- package/packages/cache/tsup.config.ts +13 -0
- package/packages/cache/vitest.config.ts +16 -0
- package/packages/config/.turbo/turbo-build.log +18 -0
- package/packages/config/dist/index.cjs +1037 -0
- package/packages/config/dist/index.cjs.map +1 -0
- package/packages/config/dist/index.d.ts +131 -0
- package/packages/config/node_modules/.bin/jiti +17 -0
- package/packages/config/node_modules/.bin/tsc +17 -0
- package/packages/config/node_modules/.bin/tsserver +17 -0
- package/packages/config/node_modules/.bin/tsup +17 -0
- package/packages/config/node_modules/.bin/tsup-node +17 -0
- package/packages/config/node_modules/.bin/tsx +17 -0
- package/packages/config/package.json +21 -0
- package/packages/config/src/index.js +1 -0
- package/packages/config/src/index.ts +1282 -0
- package/packages/config/tsconfig.json +4 -0
- package/packages/rpc/.turbo/turbo-build.log +32 -0
- package/packages/rpc/dist/_esm-BCLXDO2R.cjs +3660 -0
- package/packages/rpc/dist/_esm-BCLXDO2R.cjs.map +1 -0
- package/packages/rpc/dist/ccip-OWJLAW55.cjs +16 -0
- package/packages/rpc/dist/ccip-OWJLAW55.cjs.map +1 -0
- package/packages/rpc/dist/chunk-APQIFZ3B.cjs +6247 -0
- package/packages/rpc/dist/chunk-APQIFZ3B.cjs.map +1 -0
- package/packages/rpc/dist/chunk-CDO2GWRD.cjs +410 -0
- package/packages/rpc/dist/chunk-CDO2GWRD.cjs.map +1 -0
- package/packages/rpc/dist/chunk-QGTNTFJ7.cjs +2249 -0
- package/packages/rpc/dist/chunk-QGTNTFJ7.cjs.map +1 -0
- package/packages/rpc/dist/chunk-TZDTAHWR.cjs +44 -0
- package/packages/rpc/dist/chunk-TZDTAHWR.cjs.map +1 -0
- package/packages/rpc/dist/index.cjs +7342 -0
- package/packages/rpc/dist/index.cjs.map +1 -0
- package/packages/rpc/dist/index.d.ts +3857 -0
- package/packages/rpc/dist/secp256k1-WCNM675D.cjs +18 -0
- package/packages/rpc/dist/secp256k1-WCNM675D.cjs.map +1 -0
- package/packages/rpc/node_modules/.bin/jiti +17 -0
- package/packages/rpc/node_modules/.bin/tsc +17 -0
- package/packages/rpc/node_modules/.bin/tsserver +17 -0
- package/packages/rpc/node_modules/.bin/tsup +17 -0
- package/packages/rpc/node_modules/.bin/tsup-node +17 -0
- package/packages/rpc/node_modules/.bin/tsx +17 -0
- package/packages/rpc/package.json +25 -0
- package/packages/rpc/src/index.ts +206 -0
- package/packages/rpc/tsconfig.json +4 -0
- package/packages/typescript/base.json +36 -0
- package/packages/typescript/nextjs.json +17 -0
- package/packages/typescript/package.json +10 -0
- package/packages/ui/.turbo/turbo-build.log +44 -0
- package/packages/ui/dist/chunk-MOAFBKSA.js +11 -0
- package/packages/ui/dist/chunk-MOAFBKSA.js.map +1 -0
- package/packages/ui/dist/components/badge.d.ts +12 -0
- package/packages/ui/dist/components/badge.js +31 -0
- package/packages/ui/dist/components/badge.js.map +1 -0
- package/packages/ui/dist/components/button.d.ts +13 -0
- package/packages/ui/dist/components/button.js +40 -0
- package/packages/ui/dist/components/button.js.map +1 -0
- package/packages/ui/dist/components/card.d.ts +10 -0
- package/packages/ui/dist/components/card.js +39 -0
- package/packages/ui/dist/components/card.js.map +1 -0
- package/packages/ui/dist/components/input.d.ts +5 -0
- package/packages/ui/dist/components/input.js +28 -0
- package/packages/ui/dist/components/input.js.map +1 -0
- package/packages/ui/dist/components/label.d.ts +5 -0
- package/packages/ui/dist/components/label.js +13 -0
- package/packages/ui/dist/components/label.js.map +1 -0
- package/packages/ui/dist/components/separator.d.ts +5 -0
- package/packages/ui/dist/components/separator.js +13 -0
- package/packages/ui/dist/components/separator.js.map +1 -0
- package/packages/ui/dist/components/textarea.d.ts +5 -0
- package/packages/ui/dist/components/textarea.js +27 -0
- package/packages/ui/dist/components/textarea.js.map +1 -0
- package/packages/ui/dist/tailwind.d.ts +56 -0
- package/packages/ui/dist/tailwind.js +60 -0
- package/packages/ui/dist/tailwind.js.map +1 -0
- package/packages/ui/dist/utils/cn.d.ts +5 -0
- package/packages/ui/dist/utils/cn.js +7 -0
- package/packages/ui/dist/utils/cn.js.map +1 -0
- package/packages/ui/node_modules/.bin/jiti +17 -0
- package/packages/ui/node_modules/.bin/tsc +17 -0
- package/packages/ui/node_modules/.bin/tsserver +17 -0
- package/packages/ui/node_modules/.bin/tsup +17 -0
- package/packages/ui/node_modules/.bin/tsup-node +17 -0
- package/packages/ui/node_modules/.bin/tsx +17 -0
- package/packages/ui/package.json +69 -0
- package/packages/ui/src/components/badge.tsx +27 -0
- package/packages/ui/src/components/button.tsx +40 -0
- package/packages/ui/src/components/card.tsx +31 -0
- package/packages/ui/src/components/input.tsx +21 -0
- package/packages/ui/src/components/label.tsx +6 -0
- package/packages/ui/src/components/separator.tsx +6 -0
- package/packages/ui/src/components/textarea.tsx +20 -0
- package/packages/ui/src/globals.css +70 -0
- package/packages/ui/src/tailwind.ts +56 -0
- package/packages/ui/src/utils/cn.ts +6 -0
- package/packages/ui/tsconfig.json +20 -0
- package/packages/ui/tsup.config.ts +20 -0
- package/pnpm-workspace.yaml +4 -0
- package/scripts/install-rust-binaries.mjs +84 -0
- package/scripts/launchd/install-user-daemon.sh +358 -0
- package/scripts/launchd/run-vault-daemon.sh +5 -0
- package/scripts/launchd/run-wlfi-agent-daemon.sh +73 -0
- package/scripts/launchd/uninstall-user-daemon.sh +103 -0
- package/src/cli.ts +2121 -0
- package/src/lib/admin-guard.js +1 -0
- package/src/lib/admin-guard.ts +185 -0
- package/src/lib/admin-passthrough.ts +33 -0
- package/src/lib/admin-reset.ts +751 -0
- package/src/lib/admin-setup.ts +1612 -0
- package/src/lib/agent-auth-clear.js +1 -0
- package/src/lib/agent-auth-clear.ts +58 -0
- package/src/lib/agent-auth-forwarding.js +1 -0
- package/src/lib/agent-auth-forwarding.ts +149 -0
- package/src/lib/agent-auth-migrate.js +1 -0
- package/src/lib/agent-auth-migrate.ts +150 -0
- package/src/lib/agent-auth-revoke.ts +103 -0
- package/src/lib/agent-auth-rotate.ts +107 -0
- package/src/lib/agent-auth-token.js +1 -0
- package/src/lib/agent-auth-token.ts +25 -0
- package/src/lib/agent-auth.ts +89 -0
- package/src/lib/asset-broadcast.js +1 -0
- package/src/lib/asset-broadcast.ts +285 -0
- package/src/lib/bootstrap-artifacts.js +1 -0
- package/src/lib/bootstrap-artifacts.ts +205 -0
- package/src/lib/bootstrap-credentials.js +1 -0
- package/src/lib/bootstrap-credentials.ts +832 -0
- package/src/lib/config-amounts.js +1 -0
- package/src/lib/config-amounts.ts +189 -0
- package/src/lib/config-mutation.ts +27 -0
- package/src/lib/fs-trust.js +1 -0
- package/src/lib/fs-trust.ts +537 -0
- package/src/lib/keychain.js +1 -0
- package/src/lib/keychain.ts +225 -0
- package/src/lib/local-admin-access.ts +106 -0
- package/src/lib/network-selection.js +1 -0
- package/src/lib/network-selection.ts +71 -0
- package/src/lib/passthrough-security.js +1 -0
- package/src/lib/passthrough-security.ts +114 -0
- package/src/lib/rpc-guard.js +1 -0
- package/src/lib/rpc-guard.ts +7 -0
- package/src/lib/rust-spawn-options.js +1 -0
- package/src/lib/rust-spawn-options.ts +98 -0
- package/src/lib/rust.js +1 -0
- package/src/lib/rust.ts +143 -0
- package/src/lib/signed-tx.js +1 -0
- package/src/lib/signed-tx.ts +116 -0
- package/src/lib/status-repair-cli.ts +116 -0
- package/src/lib/sudo.js +1 -0
- package/src/lib/sudo.ts +172 -0
- package/src/lib/vault-password-forwarding.js +1 -0
- package/src/lib/vault-password-forwarding.ts +155 -0
- package/src/lib/wallet-profile.js +1 -0
- package/src/lib/wallet-profile.ts +332 -0
- package/src/lib/wallet-repair.js +1 -0
- package/src/lib/wallet-repair.ts +304 -0
- package/src/lib/wallet-setup.js +1 -0
- package/src/lib/wallet-setup.ts +1466 -0
- package/src/lib/wallet-status.js +1 -0
- package/src/lib/wallet-status.ts +640 -0
- package/tsconfig.base.json +17 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +25 -0
- package/turbo.json +41 -0
- package/LICENSE.md +0 -1
- package/dist/wlfa/index.cjs +0 -250
- package/dist/wlfa/index.d.cts +0 -1
- package/dist/wlfa/index.d.ts +0 -1
- package/dist/wlfa/index.js +0 -250
- package/dist/wlfc/index.cjs +0 -1839
- package/dist/wlfc/index.d.cts +0 -1
- package/dist/wlfc/index.d.ts +0 -1
- package/dist/wlfc/index.js +0 -1839
|
@@ -0,0 +1,1640 @@
|
|
|
1
|
+
//! Unix-domain socket transport for daemon RPC calls.
|
|
2
|
+
//!
|
|
3
|
+
//! This transport is intended for local long-running daemon deployments where
|
|
4
|
+
//! CLIs/SDK clients communicate with a separate process over a filesystem
|
|
5
|
+
//! socket path.
|
|
6
|
+
|
|
7
|
+
#![forbid(unsafe_code)]
|
|
8
|
+
|
|
9
|
+
use std::collections::BTreeSet;
|
|
10
|
+
use std::path::{Path, PathBuf};
|
|
11
|
+
use std::sync::Arc;
|
|
12
|
+
use std::time::Duration;
|
|
13
|
+
|
|
14
|
+
use async_trait::async_trait;
|
|
15
|
+
use serde::{Deserialize, Serialize};
|
|
16
|
+
use thiserror::Error;
|
|
17
|
+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
18
|
+
use tokio::net::{UnixListener, UnixStream};
|
|
19
|
+
use tokio::task::JoinHandle;
|
|
20
|
+
use uuid::Uuid;
|
|
21
|
+
use vault_daemon::{
|
|
22
|
+
DaemonError, DaemonRpcRequest, DaemonRpcResponse, InMemoryDaemon, KeyManagerDaemonApi,
|
|
23
|
+
};
|
|
24
|
+
use vault_domain::{
|
|
25
|
+
AdminSession, AgentCredentials, Lease, ManualApprovalDecision, ManualApprovalRequest,
|
|
26
|
+
NonceReleaseRequest, NonceReservation, NonceReservationRequest, PolicyAttachment, RelayConfig,
|
|
27
|
+
SignRequest, Signature, SpendingPolicy, VaultKey,
|
|
28
|
+
};
|
|
29
|
+
use vault_policy::{PolicyEvaluation, PolicyExplanation};
|
|
30
|
+
use vault_signer::{KeyCreateRequest, SignerError, VaultSignerBackend};
|
|
31
|
+
use zeroize::Zeroize;
|
|
32
|
+
|
|
33
|
+
const MAX_WIRE_BODY_BYTES: usize = 256 * 1024;
|
|
34
|
+
|
|
35
|
+
/// Validates that a client daemon socket path is an existing trusted unix socket in a secure directory.
|
|
36
|
+
pub fn assert_trusted_daemon_socket_path(path: &Path) -> Result<PathBuf, String> {
|
|
37
|
+
if path.as_os_str().is_empty() {
|
|
38
|
+
return Err("daemon socket path must not be empty".to_string());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if is_symlink(path)? {
|
|
42
|
+
return Err(format!(
|
|
43
|
+
"socket path '{}' must not be a symlink",
|
|
44
|
+
path.display()
|
|
45
|
+
));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let Some(parent) = path.parent() else {
|
|
49
|
+
return Err(format!(
|
|
50
|
+
"socket path '{}' must have a parent directory",
|
|
51
|
+
path.display()
|
|
52
|
+
));
|
|
53
|
+
};
|
|
54
|
+
if parent.as_os_str().is_empty() {
|
|
55
|
+
return Err(format!(
|
|
56
|
+
"socket path '{}' must have a parent directory",
|
|
57
|
+
path.display()
|
|
58
|
+
));
|
|
59
|
+
}
|
|
60
|
+
if is_symlink(parent)? {
|
|
61
|
+
return Err(format!(
|
|
62
|
+
"socket directory '{}' must not be a symlink",
|
|
63
|
+
parent.display()
|
|
64
|
+
));
|
|
65
|
+
}
|
|
66
|
+
ensure_secure_socket_directory(parent)?;
|
|
67
|
+
|
|
68
|
+
let metadata = std::fs::symlink_metadata(path)
|
|
69
|
+
.map_err(|err| format!("failed to inspect socket path '{}': {err}", path.display()))?;
|
|
70
|
+
#[cfg(unix)]
|
|
71
|
+
{
|
|
72
|
+
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
|
73
|
+
|
|
74
|
+
if !metadata.file_type().is_socket() {
|
|
75
|
+
return Err(format!(
|
|
76
|
+
"socket path '{}' must be a unix socket",
|
|
77
|
+
path.display()
|
|
78
|
+
));
|
|
79
|
+
}
|
|
80
|
+
let uid = metadata.uid();
|
|
81
|
+
let allowed = allowed_owner_uids()?;
|
|
82
|
+
if !allowed.contains(&uid) {
|
|
83
|
+
return Err(format!(
|
|
84
|
+
"socket path '{}' must be owned by current user, sudo caller, or root (found uid {uid})",
|
|
85
|
+
path.display(),
|
|
86
|
+
));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Ok(path.to_path_buf())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Validates that a client daemon socket path is an existing root-owned unix socket in a secure directory.
|
|
94
|
+
pub fn assert_root_owned_daemon_socket_path(path: &Path) -> Result<PathBuf, String> {
|
|
95
|
+
let resolved = assert_trusted_daemon_socket_path(path)?;
|
|
96
|
+
|
|
97
|
+
#[cfg(unix)]
|
|
98
|
+
{
|
|
99
|
+
use std::os::unix::fs::MetadataExt;
|
|
100
|
+
|
|
101
|
+
let metadata = std::fs::symlink_metadata(&resolved).map_err(|err| {
|
|
102
|
+
format!(
|
|
103
|
+
"failed to inspect socket path '{}': {err}",
|
|
104
|
+
resolved.display()
|
|
105
|
+
)
|
|
106
|
+
})?;
|
|
107
|
+
if metadata.uid() != 0 {
|
|
108
|
+
return Err(format!(
|
|
109
|
+
"socket path '{}' must be owned by root",
|
|
110
|
+
resolved.display()
|
|
111
|
+
));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Ok(resolved)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Errors returned by unix socket transport.
|
|
119
|
+
#[derive(Debug, Error)]
|
|
120
|
+
pub enum UnixTransportError {
|
|
121
|
+
/// Message serialization or deserialization failed.
|
|
122
|
+
#[error("serialization error: {0}")]
|
|
123
|
+
Serialization(String),
|
|
124
|
+
/// Protocol-level failure.
|
|
125
|
+
#[error("protocol error: {0}")]
|
|
126
|
+
Protocol(String),
|
|
127
|
+
/// Underlying daemon returned an error.
|
|
128
|
+
#[error("daemon error: {0}")]
|
|
129
|
+
Daemon(#[from] DaemonError),
|
|
130
|
+
/// Filesystem/socket operation failed.
|
|
131
|
+
#[error("io error: {0}")]
|
|
132
|
+
Io(String),
|
|
133
|
+
/// Timed out waiting for I/O.
|
|
134
|
+
#[error("transport timeout")]
|
|
135
|
+
Timeout,
|
|
136
|
+
/// Client process is not authorized by daemon peer-euid policy.
|
|
137
|
+
#[error("unauthorized peer euid (allowed {allowed:?}, got {actual})")]
|
|
138
|
+
UnauthorizedPeerEuid { allowed: Vec<u32>, actual: u32 },
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
142
|
+
struct WireRequest {
|
|
143
|
+
body_json: String,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
147
|
+
struct WireResponse {
|
|
148
|
+
ok: bool,
|
|
149
|
+
body_json: String,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
153
|
+
#[serde(tag = "kind", content = "data")]
|
|
154
|
+
enum WireDaemonError {
|
|
155
|
+
AuthenticationFailed,
|
|
156
|
+
UnknownLease,
|
|
157
|
+
InvalidLease,
|
|
158
|
+
TooManyActiveLeases,
|
|
159
|
+
UnknownVaultKey(Uuid),
|
|
160
|
+
UnknownAgentKey(Uuid),
|
|
161
|
+
UnknownPolicy(Uuid),
|
|
162
|
+
UnknownManualApprovalRequest(Uuid),
|
|
163
|
+
AgentAuthenticationFailed,
|
|
164
|
+
PayloadActionMismatch,
|
|
165
|
+
PayloadTooLarge {
|
|
166
|
+
max_bytes: usize,
|
|
167
|
+
},
|
|
168
|
+
InvalidRequestTimestamps,
|
|
169
|
+
RequestExpired,
|
|
170
|
+
RequestReplayDetected,
|
|
171
|
+
InvalidPolicyAttachment(String),
|
|
172
|
+
InvalidNonceReservation(String),
|
|
173
|
+
UnknownNonceReservation(Uuid),
|
|
174
|
+
MissingNonceReservation {
|
|
175
|
+
chain_id: u64,
|
|
176
|
+
nonce: u64,
|
|
177
|
+
},
|
|
178
|
+
InvalidPolicy(String),
|
|
179
|
+
InvalidRelayConfig(String),
|
|
180
|
+
ManualApprovalRequired {
|
|
181
|
+
approval_request_id: Uuid,
|
|
182
|
+
relay_url: Option<String>,
|
|
183
|
+
frontend_url: Option<String>,
|
|
184
|
+
},
|
|
185
|
+
ManualApprovalRejected {
|
|
186
|
+
approval_request_id: Uuid,
|
|
187
|
+
},
|
|
188
|
+
Policy(vault_policy::PolicyError),
|
|
189
|
+
Signer(SignerError),
|
|
190
|
+
PasswordHash(String),
|
|
191
|
+
InvalidConfig(String),
|
|
192
|
+
LockPoisoned,
|
|
193
|
+
Transport(String),
|
|
194
|
+
Persistence(String),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
impl From<DaemonError> for WireDaemonError {
|
|
198
|
+
fn from(value: DaemonError) -> Self {
|
|
199
|
+
match value {
|
|
200
|
+
DaemonError::AuthenticationFailed => Self::AuthenticationFailed,
|
|
201
|
+
DaemonError::UnknownLease => Self::UnknownLease,
|
|
202
|
+
DaemonError::InvalidLease => Self::InvalidLease,
|
|
203
|
+
DaemonError::TooManyActiveLeases => Self::TooManyActiveLeases,
|
|
204
|
+
DaemonError::UnknownVaultKey(id) => Self::UnknownVaultKey(id),
|
|
205
|
+
DaemonError::UnknownAgentKey(id) => Self::UnknownAgentKey(id),
|
|
206
|
+
DaemonError::UnknownPolicy(id) => Self::UnknownPolicy(id),
|
|
207
|
+
DaemonError::UnknownManualApprovalRequest(id) => Self::UnknownManualApprovalRequest(id),
|
|
208
|
+
DaemonError::AgentAuthenticationFailed => Self::AgentAuthenticationFailed,
|
|
209
|
+
DaemonError::PayloadActionMismatch => Self::PayloadActionMismatch,
|
|
210
|
+
DaemonError::PayloadTooLarge { max_bytes } => Self::PayloadTooLarge { max_bytes },
|
|
211
|
+
DaemonError::InvalidRequestTimestamps => Self::InvalidRequestTimestamps,
|
|
212
|
+
DaemonError::RequestExpired => Self::RequestExpired,
|
|
213
|
+
DaemonError::RequestReplayDetected => Self::RequestReplayDetected,
|
|
214
|
+
DaemonError::InvalidPolicyAttachment(msg) => Self::InvalidPolicyAttachment(msg),
|
|
215
|
+
DaemonError::InvalidNonceReservation(msg) => Self::InvalidNonceReservation(msg),
|
|
216
|
+
DaemonError::UnknownNonceReservation(id) => Self::UnknownNonceReservation(id),
|
|
217
|
+
DaemonError::MissingNonceReservation { chain_id, nonce } => {
|
|
218
|
+
Self::MissingNonceReservation { chain_id, nonce }
|
|
219
|
+
}
|
|
220
|
+
DaemonError::InvalidPolicy(msg) => Self::InvalidPolicy(msg),
|
|
221
|
+
DaemonError::InvalidRelayConfig(msg) => Self::InvalidRelayConfig(msg),
|
|
222
|
+
DaemonError::ManualApprovalRequired {
|
|
223
|
+
approval_request_id,
|
|
224
|
+
relay_url,
|
|
225
|
+
frontend_url,
|
|
226
|
+
} => Self::ManualApprovalRequired {
|
|
227
|
+
approval_request_id,
|
|
228
|
+
relay_url,
|
|
229
|
+
frontend_url,
|
|
230
|
+
},
|
|
231
|
+
DaemonError::ManualApprovalRejected {
|
|
232
|
+
approval_request_id,
|
|
233
|
+
} => Self::ManualApprovalRejected {
|
|
234
|
+
approval_request_id,
|
|
235
|
+
},
|
|
236
|
+
DaemonError::Policy(err) => Self::Policy(err),
|
|
237
|
+
DaemonError::Signer(err) => Self::Signer(err),
|
|
238
|
+
DaemonError::PasswordHash(msg) => Self::PasswordHash(msg),
|
|
239
|
+
DaemonError::InvalidConfig(msg) => Self::InvalidConfig(msg),
|
|
240
|
+
DaemonError::LockPoisoned => Self::LockPoisoned,
|
|
241
|
+
DaemonError::Transport(msg) => Self::Transport(msg),
|
|
242
|
+
DaemonError::Persistence(msg) => Self::Persistence(msg),
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
impl WireDaemonError {
|
|
248
|
+
fn into_daemon_error(self) -> DaemonError {
|
|
249
|
+
match self {
|
|
250
|
+
WireDaemonError::AuthenticationFailed => DaemonError::AuthenticationFailed,
|
|
251
|
+
WireDaemonError::UnknownLease => DaemonError::UnknownLease,
|
|
252
|
+
WireDaemonError::InvalidLease => DaemonError::InvalidLease,
|
|
253
|
+
WireDaemonError::TooManyActiveLeases => DaemonError::TooManyActiveLeases,
|
|
254
|
+
WireDaemonError::UnknownVaultKey(id) => DaemonError::UnknownVaultKey(id),
|
|
255
|
+
WireDaemonError::UnknownAgentKey(id) => DaemonError::UnknownAgentKey(id),
|
|
256
|
+
WireDaemonError::UnknownPolicy(id) => DaemonError::UnknownPolicy(id),
|
|
257
|
+
WireDaemonError::UnknownManualApprovalRequest(id) => {
|
|
258
|
+
DaemonError::UnknownManualApprovalRequest(id)
|
|
259
|
+
}
|
|
260
|
+
WireDaemonError::AgentAuthenticationFailed => DaemonError::AgentAuthenticationFailed,
|
|
261
|
+
WireDaemonError::PayloadActionMismatch => DaemonError::PayloadActionMismatch,
|
|
262
|
+
WireDaemonError::PayloadTooLarge { max_bytes } => {
|
|
263
|
+
DaemonError::PayloadTooLarge { max_bytes }
|
|
264
|
+
}
|
|
265
|
+
WireDaemonError::InvalidRequestTimestamps => DaemonError::InvalidRequestTimestamps,
|
|
266
|
+
WireDaemonError::RequestExpired => DaemonError::RequestExpired,
|
|
267
|
+
WireDaemonError::RequestReplayDetected => DaemonError::RequestReplayDetected,
|
|
268
|
+
WireDaemonError::InvalidPolicyAttachment(msg) => {
|
|
269
|
+
DaemonError::InvalidPolicyAttachment(msg)
|
|
270
|
+
}
|
|
271
|
+
WireDaemonError::InvalidNonceReservation(msg) => {
|
|
272
|
+
DaemonError::InvalidNonceReservation(msg)
|
|
273
|
+
}
|
|
274
|
+
WireDaemonError::UnknownNonceReservation(id) => {
|
|
275
|
+
DaemonError::UnknownNonceReservation(id)
|
|
276
|
+
}
|
|
277
|
+
WireDaemonError::MissingNonceReservation { chain_id, nonce } => {
|
|
278
|
+
DaemonError::MissingNonceReservation { chain_id, nonce }
|
|
279
|
+
}
|
|
280
|
+
WireDaemonError::InvalidPolicy(msg) => DaemonError::InvalidPolicy(msg),
|
|
281
|
+
WireDaemonError::InvalidRelayConfig(msg) => DaemonError::InvalidRelayConfig(msg),
|
|
282
|
+
WireDaemonError::ManualApprovalRequired {
|
|
283
|
+
approval_request_id,
|
|
284
|
+
relay_url,
|
|
285
|
+
frontend_url,
|
|
286
|
+
} => DaemonError::ManualApprovalRequired {
|
|
287
|
+
approval_request_id,
|
|
288
|
+
relay_url,
|
|
289
|
+
frontend_url,
|
|
290
|
+
},
|
|
291
|
+
WireDaemonError::ManualApprovalRejected {
|
|
292
|
+
approval_request_id,
|
|
293
|
+
} => DaemonError::ManualApprovalRejected {
|
|
294
|
+
approval_request_id,
|
|
295
|
+
},
|
|
296
|
+
WireDaemonError::Policy(err) => DaemonError::Policy(err),
|
|
297
|
+
WireDaemonError::Signer(err) => DaemonError::Signer(err),
|
|
298
|
+
WireDaemonError::PasswordHash(msg) => DaemonError::PasswordHash(msg),
|
|
299
|
+
WireDaemonError::InvalidConfig(msg) => DaemonError::InvalidConfig(msg),
|
|
300
|
+
WireDaemonError::LockPoisoned => DaemonError::LockPoisoned,
|
|
301
|
+
WireDaemonError::Transport(msg) => DaemonError::Transport(msg),
|
|
302
|
+
WireDaemonError::Persistence(msg) => DaemonError::Persistence(msg),
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Client adapter backed by unix-domain socket transport.
|
|
308
|
+
#[derive(Debug, Clone)]
|
|
309
|
+
pub struct UnixDaemonClient {
|
|
310
|
+
socket_path: PathBuf,
|
|
311
|
+
timeout: Duration,
|
|
312
|
+
expected_server_euid: u32,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
impl UnixDaemonClient {
|
|
316
|
+
/// Creates a unix transport client.
|
|
317
|
+
#[must_use]
|
|
318
|
+
pub fn new(socket_path: PathBuf, timeout: Duration) -> Self {
|
|
319
|
+
Self::new_with_expected_server_euid(socket_path, timeout, nix::unistd::geteuid().as_raw())
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// Creates a unix transport client with explicit expected daemon euid.
|
|
323
|
+
///
|
|
324
|
+
/// This allows hardened callers to pin daemon identity (for example `0` for root daemon).
|
|
325
|
+
#[must_use]
|
|
326
|
+
pub fn new_with_expected_server_euid(
|
|
327
|
+
socket_path: PathBuf,
|
|
328
|
+
timeout: Duration,
|
|
329
|
+
expected_server_euid: u32,
|
|
330
|
+
) -> Self {
|
|
331
|
+
Self {
|
|
332
|
+
socket_path,
|
|
333
|
+
timeout,
|
|
334
|
+
expected_server_euid,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// Performs a single daemon RPC call.
|
|
339
|
+
pub async fn call_rpc(
|
|
340
|
+
&self,
|
|
341
|
+
request: DaemonRpcRequest,
|
|
342
|
+
) -> Result<DaemonRpcResponse, UnixTransportError> {
|
|
343
|
+
let mut request = request;
|
|
344
|
+
let mut stream = tokio::time::timeout(self.timeout, UnixStream::connect(&self.socket_path))
|
|
345
|
+
.await
|
|
346
|
+
.map_err(|_| UnixTransportError::Timeout)?
|
|
347
|
+
.map_err(|err| UnixTransportError::Io(err.to_string()))?;
|
|
348
|
+
let peer_euid = peer_euid(&stream).map_err(UnixTransportError::Io)?;
|
|
349
|
+
if peer_euid != self.expected_server_euid {
|
|
350
|
+
return Err(UnixTransportError::UnauthorizedPeerEuid {
|
|
351
|
+
allowed: vec![self.expected_server_euid],
|
|
352
|
+
actual: peer_euid,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let body_json = serde_json::to_string(&request)
|
|
357
|
+
.map_err(|err| UnixTransportError::Serialization(err.to_string()));
|
|
358
|
+
request.zeroize_secrets();
|
|
359
|
+
let mut body_json = body_json?;
|
|
360
|
+
if body_json.len() > MAX_WIRE_BODY_BYTES {
|
|
361
|
+
body_json.zeroize();
|
|
362
|
+
return Err(UnixTransportError::Protocol(format!(
|
|
363
|
+
"wire request body exceeds max bytes ({MAX_WIRE_BODY_BYTES})"
|
|
364
|
+
)));
|
|
365
|
+
}
|
|
366
|
+
let mut wire_request = WireRequest { body_json };
|
|
367
|
+
let write_result = write_frame(&mut stream, &wire_request, self.timeout).await;
|
|
368
|
+
wire_request.body_json.zeroize();
|
|
369
|
+
write_result?;
|
|
370
|
+
|
|
371
|
+
let mut response: WireResponse = read_frame(&mut stream, self.timeout).await?;
|
|
372
|
+
if response.ok {
|
|
373
|
+
let parsed = serde_json::from_str::<DaemonRpcResponse>(&response.body_json)
|
|
374
|
+
.map_err(|err| UnixTransportError::Serialization(err.to_string()));
|
|
375
|
+
response.body_json.zeroize();
|
|
376
|
+
parsed
|
|
377
|
+
} else {
|
|
378
|
+
let daemon_error = match serde_json::from_str::<WireDaemonError>(&response.body_json) {
|
|
379
|
+
Ok(err) => Err(UnixTransportError::Daemon(err.into_daemon_error())),
|
|
380
|
+
Err(_) => Err(UnixTransportError::Daemon(DaemonError::Transport(
|
|
381
|
+
response.body_json.clone(),
|
|
382
|
+
))),
|
|
383
|
+
};
|
|
384
|
+
response.body_json.zeroize();
|
|
385
|
+
daemon_error
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// Unix daemon socket server bound to a local path.
|
|
391
|
+
pub struct UnixDaemonServer {
|
|
392
|
+
listener: UnixListener,
|
|
393
|
+
socket_path: PathBuf,
|
|
394
|
+
allowed_admin_peer_euids: BTreeSet<u32>,
|
|
395
|
+
allowed_agent_peer_euids: BTreeSet<u32>,
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
399
|
+
enum RpcAccessLevel {
|
|
400
|
+
Admin,
|
|
401
|
+
Agent,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
fn rpc_access_level(request: &DaemonRpcRequest) -> RpcAccessLevel {
|
|
405
|
+
match request {
|
|
406
|
+
DaemonRpcRequest::IssueLease { .. }
|
|
407
|
+
| DaemonRpcRequest::AddPolicy { .. }
|
|
408
|
+
| DaemonRpcRequest::ListPolicies { .. }
|
|
409
|
+
| DaemonRpcRequest::DisablePolicy { .. }
|
|
410
|
+
| DaemonRpcRequest::CreateVaultKey { .. }
|
|
411
|
+
| DaemonRpcRequest::CreateAgentKey { .. }
|
|
412
|
+
| DaemonRpcRequest::ExportVaultPrivateKey { .. }
|
|
413
|
+
| DaemonRpcRequest::RotateAgentAuthToken { .. }
|
|
414
|
+
| DaemonRpcRequest::RevokeAgentKey { .. }
|
|
415
|
+
| DaemonRpcRequest::ListManualApprovalRequests { .. }
|
|
416
|
+
| DaemonRpcRequest::DecideManualApprovalRequest { .. }
|
|
417
|
+
| DaemonRpcRequest::SetRelayConfig { .. }
|
|
418
|
+
| DaemonRpcRequest::GetRelayConfig { .. } => RpcAccessLevel::Admin,
|
|
419
|
+
DaemonRpcRequest::EvaluateForAgent { .. }
|
|
420
|
+
| DaemonRpcRequest::ExplainForAgent { .. }
|
|
421
|
+
| DaemonRpcRequest::ReserveNonce { .. }
|
|
422
|
+
| DaemonRpcRequest::ReleaseNonce { .. }
|
|
423
|
+
| DaemonRpcRequest::SignForAgent { .. } => RpcAccessLevel::Agent,
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
fn validate_allowed_peer_euids(
|
|
428
|
+
label: &str,
|
|
429
|
+
values: &BTreeSet<u32>,
|
|
430
|
+
) -> Result<(), UnixTransportError> {
|
|
431
|
+
if values.is_empty() {
|
|
432
|
+
return Err(UnixTransportError::Protocol(format!(
|
|
433
|
+
"allowed {label} peer euid set must not be empty"
|
|
434
|
+
)));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
Ok(())
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
fn socket_mode_for_allowed_peer_euids(
|
|
441
|
+
allowed_admin_peer_euids: &BTreeSet<u32>,
|
|
442
|
+
allowed_agent_peer_euids: &BTreeSet<u32>,
|
|
443
|
+
) -> u32 {
|
|
444
|
+
let only_root_admin =
|
|
445
|
+
allowed_admin_peer_euids.len() == 1 && allowed_admin_peer_euids.contains(&0);
|
|
446
|
+
let only_root_agent =
|
|
447
|
+
allowed_agent_peer_euids.len() == 1 && allowed_agent_peer_euids.contains(&0);
|
|
448
|
+
|
|
449
|
+
if only_root_admin && only_root_agent {
|
|
450
|
+
0o600
|
|
451
|
+
} else {
|
|
452
|
+
0o666
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
fn combined_allowed_peer_euids(
|
|
457
|
+
allowed_admin_peer_euids: &BTreeSet<u32>,
|
|
458
|
+
allowed_agent_peer_euids: &BTreeSet<u32>,
|
|
459
|
+
) -> BTreeSet<u32> {
|
|
460
|
+
allowed_admin_peer_euids
|
|
461
|
+
.union(allowed_agent_peer_euids)
|
|
462
|
+
.copied()
|
|
463
|
+
.collect()
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
impl UnixDaemonServer {
|
|
467
|
+
/// Binds server to `socket_path` and restricts clients to `allowed_peer_euids`.
|
|
468
|
+
pub async fn bind(
|
|
469
|
+
socket_path: PathBuf,
|
|
470
|
+
allowed_peer_euids: BTreeSet<u32>,
|
|
471
|
+
) -> Result<Self, UnixTransportError> {
|
|
472
|
+
Self::bind_with_allowed_peer_euids(
|
|
473
|
+
socket_path,
|
|
474
|
+
allowed_peer_euids.clone(),
|
|
475
|
+
allowed_peer_euids,
|
|
476
|
+
)
|
|
477
|
+
.await
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/// Binds server to `socket_path` with separate admin and agent peer-euid allowlists.
|
|
481
|
+
pub async fn bind_with_allowed_peer_euids(
|
|
482
|
+
socket_path: PathBuf,
|
|
483
|
+
allowed_admin_peer_euids: BTreeSet<u32>,
|
|
484
|
+
allowed_agent_peer_euids: BTreeSet<u32>,
|
|
485
|
+
) -> Result<Self, UnixTransportError> {
|
|
486
|
+
validate_allowed_peer_euids("admin", &allowed_admin_peer_euids)?;
|
|
487
|
+
validate_allowed_peer_euids("agent", &allowed_agent_peer_euids)?;
|
|
488
|
+
|
|
489
|
+
ensure_socket_parent(&socket_path).map_err(UnixTransportError::Io)?;
|
|
490
|
+
remove_existing_socket_file(&socket_path).map_err(UnixTransportError::Io)?;
|
|
491
|
+
|
|
492
|
+
let listener = UnixListener::bind(&socket_path)
|
|
493
|
+
.map_err(|err| UnixTransportError::Io(err.to_string()))?;
|
|
494
|
+
#[cfg(unix)]
|
|
495
|
+
{
|
|
496
|
+
use std::os::unix::fs::PermissionsExt;
|
|
497
|
+
let socket_mode = socket_mode_for_allowed_peer_euids(
|
|
498
|
+
&allowed_admin_peer_euids,
|
|
499
|
+
&allowed_agent_peer_euids,
|
|
500
|
+
);
|
|
501
|
+
std::fs::set_permissions(&socket_path, std::fs::Permissions::from_mode(socket_mode))
|
|
502
|
+
.map_err(|err| UnixTransportError::Io(err.to_string()))?;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
Ok(Self {
|
|
506
|
+
listener,
|
|
507
|
+
socket_path,
|
|
508
|
+
allowed_admin_peer_euids,
|
|
509
|
+
allowed_agent_peer_euids,
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/// Runs accept loop until `shutdown` resolves.
|
|
514
|
+
pub async fn run_until_shutdown<B, S>(
|
|
515
|
+
self,
|
|
516
|
+
daemon: Arc<InMemoryDaemon<B>>,
|
|
517
|
+
shutdown: S,
|
|
518
|
+
) -> Result<(), UnixTransportError>
|
|
519
|
+
where
|
|
520
|
+
B: VaultSignerBackend + 'static,
|
|
521
|
+
S: std::future::Future<Output = ()>,
|
|
522
|
+
{
|
|
523
|
+
tokio::pin!(shutdown);
|
|
524
|
+
let mut workers: Vec<JoinHandle<()>> = Vec::new();
|
|
525
|
+
loop {
|
|
526
|
+
tokio::select! {
|
|
527
|
+
_ = &mut shutdown => {
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
accept_result = self.listener.accept() => {
|
|
531
|
+
let (stream, _) = accept_result
|
|
532
|
+
.map_err(|err| UnixTransportError::Io(err.to_string()))?;
|
|
533
|
+
let daemon = daemon.clone();
|
|
534
|
+
let allowed_admin_peer_euids = self.allowed_admin_peer_euids.clone();
|
|
535
|
+
let allowed_agent_peer_euids = self.allowed_agent_peer_euids.clone();
|
|
536
|
+
workers.push(tokio::spawn(async move {
|
|
537
|
+
let _ = handle_connection(
|
|
538
|
+
stream,
|
|
539
|
+
daemon,
|
|
540
|
+
allowed_admin_peer_euids,
|
|
541
|
+
allowed_agent_peer_euids,
|
|
542
|
+
)
|
|
543
|
+
.await;
|
|
544
|
+
}));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
for worker in workers {
|
|
550
|
+
let _ = worker.await;
|
|
551
|
+
}
|
|
552
|
+
Ok(())
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/// Returns bound socket path.
|
|
556
|
+
#[must_use]
|
|
557
|
+
pub fn socket_path(&self) -> &Path {
|
|
558
|
+
&self.socket_path
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
impl Drop for UnixDaemonServer {
|
|
563
|
+
fn drop(&mut self) {
|
|
564
|
+
let _ = std::fs::remove_file(&self.socket_path);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async fn handle_connection<B>(
|
|
569
|
+
mut stream: UnixStream,
|
|
570
|
+
daemon: Arc<InMemoryDaemon<B>>,
|
|
571
|
+
allowed_admin_peer_euids: BTreeSet<u32>,
|
|
572
|
+
allowed_agent_peer_euids: BTreeSet<u32>,
|
|
573
|
+
) -> Result<(), UnixTransportError>
|
|
574
|
+
where
|
|
575
|
+
B: VaultSignerBackend + 'static,
|
|
576
|
+
{
|
|
577
|
+
let peer_euid = peer_euid(&stream).map_err(UnixTransportError::Io)?;
|
|
578
|
+
let globally_allowed_peer_euids =
|
|
579
|
+
combined_allowed_peer_euids(&allowed_admin_peer_euids, &allowed_agent_peer_euids);
|
|
580
|
+
if !globally_allowed_peer_euids.contains(&peer_euid) {
|
|
581
|
+
return Err(UnixTransportError::UnauthorizedPeerEuid {
|
|
582
|
+
allowed: globally_allowed_peer_euids.into_iter().collect(),
|
|
583
|
+
actual: peer_euid,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let mut request: WireRequest = read_frame(&mut stream, Duration::from_secs(10)).await?;
|
|
588
|
+
if request.body_json.len() > MAX_WIRE_BODY_BYTES {
|
|
589
|
+
request.body_json.zeroize();
|
|
590
|
+
return Err(UnixTransportError::Protocol(format!(
|
|
591
|
+
"wire request body exceeds max bytes ({MAX_WIRE_BODY_BYTES})"
|
|
592
|
+
)));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
let daemon_request = serde_json::from_str(&request.body_json)
|
|
596
|
+
.map_err(|err| UnixTransportError::Serialization(err.to_string()));
|
|
597
|
+
request.body_json.zeroize();
|
|
598
|
+
let mut daemon_request: DaemonRpcRequest = daemon_request?;
|
|
599
|
+
let allowed_peer_euids = match rpc_access_level(&daemon_request) {
|
|
600
|
+
RpcAccessLevel::Admin => &allowed_admin_peer_euids,
|
|
601
|
+
RpcAccessLevel::Agent => &allowed_agent_peer_euids,
|
|
602
|
+
};
|
|
603
|
+
if !allowed_peer_euids.contains(&peer_euid) {
|
|
604
|
+
daemon_request.zeroize_secrets();
|
|
605
|
+
return Err(UnixTransportError::UnauthorizedPeerEuid {
|
|
606
|
+
allowed: allowed_peer_euids.iter().copied().collect(),
|
|
607
|
+
actual: peer_euid,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
let response = match daemon.handle_rpc(daemon_request).await {
|
|
612
|
+
Ok(success) => {
|
|
613
|
+
let mut success = success;
|
|
614
|
+
let body_json = serde_json::to_string(&success)
|
|
615
|
+
.map_err(|err| UnixTransportError::Serialization(err.to_string()));
|
|
616
|
+
success.zeroize_secrets();
|
|
617
|
+
WireResponse {
|
|
618
|
+
ok: true,
|
|
619
|
+
body_json: body_json?,
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
Err(err) => WireResponse {
|
|
623
|
+
ok: false,
|
|
624
|
+
body_json: serde_json::to_string(&WireDaemonError::from(err))
|
|
625
|
+
.map_err(|ser| UnixTransportError::Serialization(ser.to_string()))?,
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
let mut response = response;
|
|
629
|
+
let write_result = write_frame(&mut stream, &response, Duration::from_secs(10)).await;
|
|
630
|
+
response.body_json.zeroize();
|
|
631
|
+
write_result?;
|
|
632
|
+
Ok(())
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async fn write_frame<T: Serialize>(
|
|
636
|
+
stream: &mut UnixStream,
|
|
637
|
+
value: &T,
|
|
638
|
+
timeout: Duration,
|
|
639
|
+
) -> Result<(), UnixTransportError> {
|
|
640
|
+
let mut payload = serde_json::to_vec(value)
|
|
641
|
+
.map_err(|err| UnixTransportError::Serialization(err.to_string()))?;
|
|
642
|
+
if payload.len() > MAX_WIRE_BODY_BYTES {
|
|
643
|
+
payload.zeroize();
|
|
644
|
+
return Err(UnixTransportError::Protocol(format!(
|
|
645
|
+
"wire body exceeds max bytes ({MAX_WIRE_BODY_BYTES})"
|
|
646
|
+
)));
|
|
647
|
+
}
|
|
648
|
+
let len = payload.len() as u32;
|
|
649
|
+
let result = async {
|
|
650
|
+
tokio::time::timeout(timeout, stream.write_all(&len.to_be_bytes()))
|
|
651
|
+
.await
|
|
652
|
+
.map_err(|_| UnixTransportError::Timeout)?
|
|
653
|
+
.map_err(|err| UnixTransportError::Io(err.to_string()))?;
|
|
654
|
+
tokio::time::timeout(timeout, stream.write_all(&payload))
|
|
655
|
+
.await
|
|
656
|
+
.map_err(|_| UnixTransportError::Timeout)?
|
|
657
|
+
.map_err(|err| UnixTransportError::Io(err.to_string()))?;
|
|
658
|
+
Ok(())
|
|
659
|
+
}
|
|
660
|
+
.await;
|
|
661
|
+
payload.zeroize();
|
|
662
|
+
result
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async fn read_frame<T: for<'de> Deserialize<'de>>(
|
|
666
|
+
stream: &mut UnixStream,
|
|
667
|
+
timeout: Duration,
|
|
668
|
+
) -> Result<T, UnixTransportError> {
|
|
669
|
+
let mut len_buf = [0u8; 4];
|
|
670
|
+
tokio::time::timeout(timeout, stream.read_exact(&mut len_buf))
|
|
671
|
+
.await
|
|
672
|
+
.map_err(|_| UnixTransportError::Timeout)?
|
|
673
|
+
.map_err(|err| UnixTransportError::Io(err.to_string()))?;
|
|
674
|
+
let len = u32::from_be_bytes(len_buf) as usize;
|
|
675
|
+
if len > MAX_WIRE_BODY_BYTES {
|
|
676
|
+
return Err(UnixTransportError::Protocol(format!(
|
|
677
|
+
"wire frame exceeds max bytes ({MAX_WIRE_BODY_BYTES})"
|
|
678
|
+
)));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let mut payload = vec![0u8; len];
|
|
682
|
+
let result = async {
|
|
683
|
+
tokio::time::timeout(timeout, stream.read_exact(&mut payload))
|
|
684
|
+
.await
|
|
685
|
+
.map_err(|_| UnixTransportError::Timeout)?
|
|
686
|
+
.map_err(|err| UnixTransportError::Io(err.to_string()))?;
|
|
687
|
+
serde_json::from_slice::<T>(&payload)
|
|
688
|
+
.map_err(|err| UnixTransportError::Serialization(err.to_string()))
|
|
689
|
+
}
|
|
690
|
+
.await;
|
|
691
|
+
payload.zeroize();
|
|
692
|
+
result
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
fn ensure_socket_parent(path: &Path) -> Result<(), String> {
|
|
696
|
+
if is_symlink(path)? {
|
|
697
|
+
return Err(format!(
|
|
698
|
+
"socket path '{}' must not be a symlink",
|
|
699
|
+
path.display()
|
|
700
|
+
));
|
|
701
|
+
}
|
|
702
|
+
if let Some(parent) = path.parent() {
|
|
703
|
+
if !parent.as_os_str().is_empty() {
|
|
704
|
+
std::fs::create_dir_all(parent).map_err(|err| {
|
|
705
|
+
format!(
|
|
706
|
+
"failed to create socket directory '{}': {err}",
|
|
707
|
+
parent.display()
|
|
708
|
+
)
|
|
709
|
+
})?;
|
|
710
|
+
if is_symlink(parent)? {
|
|
711
|
+
return Err(format!(
|
|
712
|
+
"socket directory '{}' must not be a symlink",
|
|
713
|
+
parent.display()
|
|
714
|
+
));
|
|
715
|
+
}
|
|
716
|
+
ensure_secure_socket_directory(parent)?;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
Ok(())
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
#[cfg(unix)]
|
|
723
|
+
fn ensure_secure_socket_directory(path: &Path) -> Result<(), String> {
|
|
724
|
+
use std::os::unix::fs::MetadataExt;
|
|
725
|
+
|
|
726
|
+
const GROUP_OTHER_WRITE_MODE_MASK: u32 = 0o022;
|
|
727
|
+
const STICKY_BIT_MODE: u32 = 0o1000;
|
|
728
|
+
|
|
729
|
+
fn allowed_owner_uids() -> Result<BTreeSet<u32>, String> {
|
|
730
|
+
let mut allowed = BTreeSet::new();
|
|
731
|
+
allowed.insert(nix::unistd::geteuid().as_raw());
|
|
732
|
+
if nix::unistd::geteuid().as_raw() == 0 {
|
|
733
|
+
if let Some(raw) = std::env::var_os("SUDO_UID") {
|
|
734
|
+
let rendered = raw.to_string_lossy();
|
|
735
|
+
let parsed = rendered
|
|
736
|
+
.parse::<u32>()
|
|
737
|
+
.map_err(|_| format!("invalid SUDO_UID value '{rendered}'"))?;
|
|
738
|
+
allowed.insert(parsed);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
Ok(allowed)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
fn validate_directory(
|
|
745
|
+
path: &Path,
|
|
746
|
+
metadata: &std::fs::Metadata,
|
|
747
|
+
allow_sticky_group_other_write: bool,
|
|
748
|
+
) -> Result<(), String> {
|
|
749
|
+
if !metadata.is_dir() {
|
|
750
|
+
return Err(format!(
|
|
751
|
+
"socket directory '{}' must be a directory",
|
|
752
|
+
path.display()
|
|
753
|
+
));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
let uid = metadata.uid();
|
|
757
|
+
if uid != 0 {
|
|
758
|
+
let allowed = allowed_owner_uids()?;
|
|
759
|
+
if !allowed.contains(&uid) {
|
|
760
|
+
return Err(format!(
|
|
761
|
+
"socket directory '{}' must be owned by current user, sudo caller, or root (found uid {uid})",
|
|
762
|
+
path.display()
|
|
763
|
+
));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
let mode = metadata.mode() & 0o7777;
|
|
768
|
+
if mode & GROUP_OTHER_WRITE_MODE_MASK != 0
|
|
769
|
+
&& !(allow_sticky_group_other_write && mode & STICKY_BIT_MODE != 0)
|
|
770
|
+
{
|
|
771
|
+
return Err(format!(
|
|
772
|
+
"socket directory '{}' must not be writable by group/other",
|
|
773
|
+
path.display()
|
|
774
|
+
));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
Ok(())
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
let metadata = std::fs::metadata(path).map_err(|err| {
|
|
781
|
+
format!(
|
|
782
|
+
"failed to inspect socket directory '{}': {err}",
|
|
783
|
+
path.display()
|
|
784
|
+
)
|
|
785
|
+
})?;
|
|
786
|
+
validate_directory(path, &metadata, false)?;
|
|
787
|
+
|
|
788
|
+
let canonical = std::fs::canonicalize(path).map_err(|err| {
|
|
789
|
+
format!(
|
|
790
|
+
"failed to canonicalize socket directory '{}': {err}",
|
|
791
|
+
path.display()
|
|
792
|
+
)
|
|
793
|
+
})?;
|
|
794
|
+
for ancestor in canonical.ancestors().skip(1) {
|
|
795
|
+
let metadata = std::fs::metadata(ancestor).map_err(|err| {
|
|
796
|
+
format!(
|
|
797
|
+
"failed to inspect ancestor socket directory '{}': {err}",
|
|
798
|
+
ancestor.display()
|
|
799
|
+
)
|
|
800
|
+
})?;
|
|
801
|
+
validate_directory(ancestor, &metadata, true)?;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
Ok(())
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
#[cfg(unix)]
|
|
808
|
+
fn allowed_owner_uids() -> Result<BTreeSet<u32>, String> {
|
|
809
|
+
let mut allowed = BTreeSet::new();
|
|
810
|
+
allowed.insert(nix::unistd::geteuid().as_raw());
|
|
811
|
+
allowed.insert(0);
|
|
812
|
+
if nix::unistd::geteuid().as_raw() == 0 {
|
|
813
|
+
if let Some(raw) = std::env::var_os("SUDO_UID") {
|
|
814
|
+
let rendered = raw.to_string_lossy();
|
|
815
|
+
let parsed = rendered
|
|
816
|
+
.parse::<u32>()
|
|
817
|
+
.map_err(|_| format!("invalid SUDO_UID value '{rendered}'"))?;
|
|
818
|
+
allowed.insert(parsed);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
Ok(allowed)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
#[cfg(not(unix))]
|
|
825
|
+
fn ensure_secure_socket_directory(_path: &Path) -> Result<(), String> {
|
|
826
|
+
Ok(())
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
fn remove_existing_socket_file(path: &Path) -> Result<(), String> {
|
|
830
|
+
if !path.exists() {
|
|
831
|
+
return Ok(());
|
|
832
|
+
}
|
|
833
|
+
let metadata = std::fs::symlink_metadata(path)
|
|
834
|
+
.map_err(|err| format!("failed to inspect socket path '{}': {err}", path.display()))?;
|
|
835
|
+
#[cfg(unix)]
|
|
836
|
+
{
|
|
837
|
+
use std::os::unix::fs::FileTypeExt;
|
|
838
|
+
if !metadata.file_type().is_socket() {
|
|
839
|
+
return Err(format!(
|
|
840
|
+
"socket path '{}' exists and is not a unix socket",
|
|
841
|
+
path.display()
|
|
842
|
+
));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
std::fs::remove_file(path)
|
|
846
|
+
.map_err(|err| format!("failed to remove stale socket '{}': {err}", path.display()))
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
fn is_symlink(path: &Path) -> Result<bool, String> {
|
|
850
|
+
match std::fs::symlink_metadata(path) {
|
|
851
|
+
Ok(metadata) => Ok(metadata.file_type().is_symlink()),
|
|
852
|
+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
|
853
|
+
Err(err) => Err(format!(
|
|
854
|
+
"failed to inspect metadata for '{}': {err}",
|
|
855
|
+
path.display()
|
|
856
|
+
)),
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
fn peer_euid(stream: &UnixStream) -> Result<u32, String> {
|
|
861
|
+
let creds = stream.peer_cred().map_err(|err| err.to_string())?;
|
|
862
|
+
Ok(creds.uid())
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
#[async_trait]
|
|
866
|
+
impl KeyManagerDaemonApi for UnixDaemonClient {
|
|
867
|
+
async fn issue_lease(&self, vault_password: &str) -> Result<Lease, DaemonError> {
|
|
868
|
+
match self
|
|
869
|
+
.call_rpc(DaemonRpcRequest::IssueLease {
|
|
870
|
+
vault_password: vault_password.to_string(),
|
|
871
|
+
})
|
|
872
|
+
.await
|
|
873
|
+
{
|
|
874
|
+
Ok(DaemonRpcResponse::Lease(lease)) => Ok(lease),
|
|
875
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
876
|
+
"unexpected response type".to_string(),
|
|
877
|
+
)),
|
|
878
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
879
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async fn add_policy(
|
|
884
|
+
&self,
|
|
885
|
+
session: &AdminSession,
|
|
886
|
+
policy: SpendingPolicy,
|
|
887
|
+
) -> Result<(), DaemonError> {
|
|
888
|
+
match self
|
|
889
|
+
.call_rpc(DaemonRpcRequest::AddPolicy {
|
|
890
|
+
session: session.clone(),
|
|
891
|
+
policy,
|
|
892
|
+
})
|
|
893
|
+
.await
|
|
894
|
+
{
|
|
895
|
+
Ok(DaemonRpcResponse::Unit) => Ok(()),
|
|
896
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
897
|
+
"unexpected response type".to_string(),
|
|
898
|
+
)),
|
|
899
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
900
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
async fn list_policies(
|
|
905
|
+
&self,
|
|
906
|
+
session: &AdminSession,
|
|
907
|
+
) -> Result<Vec<SpendingPolicy>, DaemonError> {
|
|
908
|
+
match self
|
|
909
|
+
.call_rpc(DaemonRpcRequest::ListPolicies {
|
|
910
|
+
session: session.clone(),
|
|
911
|
+
})
|
|
912
|
+
.await
|
|
913
|
+
{
|
|
914
|
+
Ok(DaemonRpcResponse::Policies(policies)) => Ok(policies),
|
|
915
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
916
|
+
"unexpected response type".to_string(),
|
|
917
|
+
)),
|
|
918
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
919
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async fn disable_policy(
|
|
924
|
+
&self,
|
|
925
|
+
session: &AdminSession,
|
|
926
|
+
policy_id: Uuid,
|
|
927
|
+
) -> Result<(), DaemonError> {
|
|
928
|
+
match self
|
|
929
|
+
.call_rpc(DaemonRpcRequest::DisablePolicy {
|
|
930
|
+
session: session.clone(),
|
|
931
|
+
policy_id,
|
|
932
|
+
})
|
|
933
|
+
.await
|
|
934
|
+
{
|
|
935
|
+
Ok(DaemonRpcResponse::Unit) => Ok(()),
|
|
936
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
937
|
+
"unexpected response type".to_string(),
|
|
938
|
+
)),
|
|
939
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
940
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async fn create_vault_key(
|
|
945
|
+
&self,
|
|
946
|
+
session: &AdminSession,
|
|
947
|
+
request: KeyCreateRequest,
|
|
948
|
+
) -> Result<VaultKey, DaemonError> {
|
|
949
|
+
match self
|
|
950
|
+
.call_rpc(DaemonRpcRequest::CreateVaultKey {
|
|
951
|
+
session: session.clone(),
|
|
952
|
+
request,
|
|
953
|
+
})
|
|
954
|
+
.await
|
|
955
|
+
{
|
|
956
|
+
Ok(DaemonRpcResponse::VaultKey(key)) => Ok(key),
|
|
957
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
958
|
+
"unexpected response type".to_string(),
|
|
959
|
+
)),
|
|
960
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
961
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
async fn create_agent_key(
|
|
966
|
+
&self,
|
|
967
|
+
session: &AdminSession,
|
|
968
|
+
vault_key_id: Uuid,
|
|
969
|
+
attachment: PolicyAttachment,
|
|
970
|
+
) -> Result<AgentCredentials, DaemonError> {
|
|
971
|
+
match self
|
|
972
|
+
.call_rpc(DaemonRpcRequest::CreateAgentKey {
|
|
973
|
+
session: session.clone(),
|
|
974
|
+
vault_key_id,
|
|
975
|
+
attachment,
|
|
976
|
+
})
|
|
977
|
+
.await
|
|
978
|
+
{
|
|
979
|
+
Ok(DaemonRpcResponse::AgentCredentials(agent_credentials)) => Ok(agent_credentials),
|
|
980
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
981
|
+
"unexpected response type".to_string(),
|
|
982
|
+
)),
|
|
983
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
984
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async fn export_vault_private_key(
|
|
989
|
+
&self,
|
|
990
|
+
session: &AdminSession,
|
|
991
|
+
vault_key_id: Uuid,
|
|
992
|
+
) -> Result<Option<String>, DaemonError> {
|
|
993
|
+
match self
|
|
994
|
+
.call_rpc(DaemonRpcRequest::ExportVaultPrivateKey {
|
|
995
|
+
session: session.clone(),
|
|
996
|
+
vault_key_id,
|
|
997
|
+
})
|
|
998
|
+
.await
|
|
999
|
+
{
|
|
1000
|
+
Ok(DaemonRpcResponse::PrivateKey(private_key)) => Ok(private_key),
|
|
1001
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1002
|
+
"unexpected response type".to_string(),
|
|
1003
|
+
)),
|
|
1004
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1005
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async fn rotate_agent_auth_token(
|
|
1010
|
+
&self,
|
|
1011
|
+
session: &AdminSession,
|
|
1012
|
+
agent_key_id: Uuid,
|
|
1013
|
+
) -> Result<String, DaemonError> {
|
|
1014
|
+
match self
|
|
1015
|
+
.call_rpc(DaemonRpcRequest::RotateAgentAuthToken {
|
|
1016
|
+
session: session.clone(),
|
|
1017
|
+
agent_key_id,
|
|
1018
|
+
})
|
|
1019
|
+
.await
|
|
1020
|
+
{
|
|
1021
|
+
Ok(DaemonRpcResponse::AuthToken(token)) => Ok(token),
|
|
1022
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1023
|
+
"unexpected response type".to_string(),
|
|
1024
|
+
)),
|
|
1025
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1026
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
async fn revoke_agent_key(
|
|
1031
|
+
&self,
|
|
1032
|
+
session: &AdminSession,
|
|
1033
|
+
agent_key_id: Uuid,
|
|
1034
|
+
) -> Result<(), DaemonError> {
|
|
1035
|
+
match self
|
|
1036
|
+
.call_rpc(DaemonRpcRequest::RevokeAgentKey {
|
|
1037
|
+
session: session.clone(),
|
|
1038
|
+
agent_key_id,
|
|
1039
|
+
})
|
|
1040
|
+
.await
|
|
1041
|
+
{
|
|
1042
|
+
Ok(DaemonRpcResponse::Unit) => Ok(()),
|
|
1043
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1044
|
+
"unexpected response type".to_string(),
|
|
1045
|
+
)),
|
|
1046
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1047
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
async fn list_manual_approval_requests(
|
|
1052
|
+
&self,
|
|
1053
|
+
session: &AdminSession,
|
|
1054
|
+
) -> Result<Vec<ManualApprovalRequest>, DaemonError> {
|
|
1055
|
+
match self
|
|
1056
|
+
.call_rpc(DaemonRpcRequest::ListManualApprovalRequests {
|
|
1057
|
+
session: session.clone(),
|
|
1058
|
+
})
|
|
1059
|
+
.await
|
|
1060
|
+
{
|
|
1061
|
+
Ok(DaemonRpcResponse::ManualApprovalRequests(requests)) => Ok(requests),
|
|
1062
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1063
|
+
"unexpected response type".to_string(),
|
|
1064
|
+
)),
|
|
1065
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1066
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async fn decide_manual_approval_request(
|
|
1071
|
+
&self,
|
|
1072
|
+
session: &AdminSession,
|
|
1073
|
+
approval_request_id: Uuid,
|
|
1074
|
+
decision: ManualApprovalDecision,
|
|
1075
|
+
rejection_reason: Option<String>,
|
|
1076
|
+
) -> Result<ManualApprovalRequest, DaemonError> {
|
|
1077
|
+
match self
|
|
1078
|
+
.call_rpc(DaemonRpcRequest::DecideManualApprovalRequest {
|
|
1079
|
+
session: session.clone(),
|
|
1080
|
+
approval_request_id,
|
|
1081
|
+
decision,
|
|
1082
|
+
rejection_reason,
|
|
1083
|
+
})
|
|
1084
|
+
.await
|
|
1085
|
+
{
|
|
1086
|
+
Ok(DaemonRpcResponse::ManualApprovalRequest(request)) => Ok(request),
|
|
1087
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1088
|
+
"unexpected response type".to_string(),
|
|
1089
|
+
)),
|
|
1090
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1091
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async fn set_relay_config(
|
|
1096
|
+
&self,
|
|
1097
|
+
session: &AdminSession,
|
|
1098
|
+
relay_url: Option<String>,
|
|
1099
|
+
frontend_url: Option<String>,
|
|
1100
|
+
) -> Result<RelayConfig, DaemonError> {
|
|
1101
|
+
match self
|
|
1102
|
+
.call_rpc(DaemonRpcRequest::SetRelayConfig {
|
|
1103
|
+
session: session.clone(),
|
|
1104
|
+
relay_url,
|
|
1105
|
+
frontend_url,
|
|
1106
|
+
})
|
|
1107
|
+
.await
|
|
1108
|
+
{
|
|
1109
|
+
Ok(DaemonRpcResponse::RelayConfig(relay_config)) => Ok(relay_config),
|
|
1110
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1111
|
+
"unexpected response type".to_string(),
|
|
1112
|
+
)),
|
|
1113
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1114
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
async fn get_relay_config(&self, session: &AdminSession) -> Result<RelayConfig, DaemonError> {
|
|
1119
|
+
match self
|
|
1120
|
+
.call_rpc(DaemonRpcRequest::GetRelayConfig {
|
|
1121
|
+
session: session.clone(),
|
|
1122
|
+
})
|
|
1123
|
+
.await
|
|
1124
|
+
{
|
|
1125
|
+
Ok(DaemonRpcResponse::RelayConfig(relay_config)) => Ok(relay_config),
|
|
1126
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1127
|
+
"unexpected response type".to_string(),
|
|
1128
|
+
)),
|
|
1129
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1130
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async fn evaluate_for_agent(
|
|
1135
|
+
&self,
|
|
1136
|
+
request: SignRequest,
|
|
1137
|
+
) -> Result<PolicyEvaluation, DaemonError> {
|
|
1138
|
+
match self
|
|
1139
|
+
.call_rpc(DaemonRpcRequest::EvaluateForAgent { request })
|
|
1140
|
+
.await
|
|
1141
|
+
{
|
|
1142
|
+
Ok(DaemonRpcResponse::PolicyEvaluation(evaluation)) => Ok(evaluation),
|
|
1143
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1144
|
+
"unexpected response type".to_string(),
|
|
1145
|
+
)),
|
|
1146
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1147
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
async fn explain_for_agent(
|
|
1152
|
+
&self,
|
|
1153
|
+
request: SignRequest,
|
|
1154
|
+
) -> Result<PolicyExplanation, DaemonError> {
|
|
1155
|
+
match self
|
|
1156
|
+
.call_rpc(DaemonRpcRequest::ExplainForAgent { request })
|
|
1157
|
+
.await
|
|
1158
|
+
{
|
|
1159
|
+
Ok(DaemonRpcResponse::PolicyExplanation(explanation)) => Ok(explanation),
|
|
1160
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1161
|
+
"unexpected response type".to_string(),
|
|
1162
|
+
)),
|
|
1163
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1164
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async fn reserve_nonce(
|
|
1169
|
+
&self,
|
|
1170
|
+
request: NonceReservationRequest,
|
|
1171
|
+
) -> Result<NonceReservation, DaemonError> {
|
|
1172
|
+
match self
|
|
1173
|
+
.call_rpc(DaemonRpcRequest::ReserveNonce { request })
|
|
1174
|
+
.await
|
|
1175
|
+
{
|
|
1176
|
+
Ok(DaemonRpcResponse::NonceReservation(reservation)) => Ok(reservation),
|
|
1177
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1178
|
+
"unexpected response type".to_string(),
|
|
1179
|
+
)),
|
|
1180
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1181
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
async fn release_nonce(&self, request: NonceReleaseRequest) -> Result<(), DaemonError> {
|
|
1186
|
+
match self
|
|
1187
|
+
.call_rpc(DaemonRpcRequest::ReleaseNonce { request })
|
|
1188
|
+
.await
|
|
1189
|
+
{
|
|
1190
|
+
Ok(DaemonRpcResponse::Unit) => Ok(()),
|
|
1191
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1192
|
+
"unexpected response type".to_string(),
|
|
1193
|
+
)),
|
|
1194
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1195
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
async fn sign_for_agent(&self, request: SignRequest) -> Result<Signature, DaemonError> {
|
|
1200
|
+
match self
|
|
1201
|
+
.call_rpc(DaemonRpcRequest::SignForAgent { request })
|
|
1202
|
+
.await
|
|
1203
|
+
{
|
|
1204
|
+
Ok(DaemonRpcResponse::Signature(sig)) => Ok(sig),
|
|
1205
|
+
Ok(_) => Err(DaemonError::Transport(
|
|
1206
|
+
"unexpected response type".to_string(),
|
|
1207
|
+
)),
|
|
1208
|
+
Err(UnixTransportError::Daemon(err)) => Err(err),
|
|
1209
|
+
Err(err) => Err(DaemonError::Transport(err.to_string())),
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
#[cfg(test)]
|
|
1215
|
+
mod tests {
|
|
1216
|
+
use super::{
|
|
1217
|
+
assert_root_owned_daemon_socket_path, combined_allowed_peer_euids, ensure_socket_parent,
|
|
1218
|
+
handle_connection, read_frame, rpc_access_level, socket_mode_for_allowed_peer_euids,
|
|
1219
|
+
write_frame, RpcAccessLevel, UnixDaemonClient, UnixDaemonServer, UnixTransportError,
|
|
1220
|
+
WireRequest, WireResponse,
|
|
1221
|
+
};
|
|
1222
|
+
use std::collections::BTreeSet;
|
|
1223
|
+
use std::path::Path;
|
|
1224
|
+
use std::sync::Arc;
|
|
1225
|
+
use std::time::Duration;
|
|
1226
|
+
use time::OffsetDateTime;
|
|
1227
|
+
use tokio::net::UnixStream;
|
|
1228
|
+
use uuid::Uuid;
|
|
1229
|
+
use vault_daemon::{DaemonConfig, InMemoryDaemon, KeyManagerDaemonApi};
|
|
1230
|
+
use vault_domain::{AgentAction, SignRequest};
|
|
1231
|
+
use vault_signer::SoftwareSignerBackend;
|
|
1232
|
+
|
|
1233
|
+
fn unique_socket_path(label: &str) -> std::path::PathBuf {
|
|
1234
|
+
std::env::temp_dir().join(format!(
|
|
1235
|
+
"wlfi-{}-{}-{}.sock",
|
|
1236
|
+
label,
|
|
1237
|
+
std::process::id(),
|
|
1238
|
+
&uuid::Uuid::new_v4().simple().to_string()[..8]
|
|
1239
|
+
))
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
fn singleton_allowed_set(euid: u32) -> BTreeSet<u32> {
|
|
1243
|
+
let mut set = BTreeSet::new();
|
|
1244
|
+
set.insert(euid);
|
|
1245
|
+
set
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
fn dummy_agent_request() -> SignRequest {
|
|
1249
|
+
SignRequest {
|
|
1250
|
+
request_id: Uuid::new_v4(),
|
|
1251
|
+
agent_key_id: Uuid::new_v4(),
|
|
1252
|
+
agent_auth_token: "agent-secret".to_string(),
|
|
1253
|
+
payload: vec![1, 2, 3],
|
|
1254
|
+
action: AgentAction::TransferNative {
|
|
1255
|
+
chain_id: 1,
|
|
1256
|
+
to: "0x2222222222222222222222222222222222222222"
|
|
1257
|
+
.parse()
|
|
1258
|
+
.expect("recipient"),
|
|
1259
|
+
amount_wei: 1,
|
|
1260
|
+
},
|
|
1261
|
+
requested_at: OffsetDateTime::now_utc(),
|
|
1262
|
+
expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(1),
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
#[cfg(unix)]
|
|
1267
|
+
fn socket_mode(path: &Path) -> u32 {
|
|
1268
|
+
use std::os::unix::fs::PermissionsExt;
|
|
1269
|
+
|
|
1270
|
+
std::fs::metadata(path)
|
|
1271
|
+
.expect("socket metadata")
|
|
1272
|
+
.permissions()
|
|
1273
|
+
.mode()
|
|
1274
|
+
& 0o777
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
#[test]
|
|
1278
|
+
#[cfg(unix)]
|
|
1279
|
+
fn ensure_socket_parent_rejects_group_writable_ancestor_directory() {
|
|
1280
|
+
use std::os::unix::fs::PermissionsExt;
|
|
1281
|
+
|
|
1282
|
+
let root = std::env::temp_dir().join(format!(
|
|
1283
|
+
"wlfi-socket-parent-ancestor-{}-{}",
|
|
1284
|
+
std::process::id(),
|
|
1285
|
+
uuid::Uuid::new_v4().simple()
|
|
1286
|
+
));
|
|
1287
|
+
let shared = root.join("shared");
|
|
1288
|
+
let nested = shared.join("nested");
|
|
1289
|
+
std::fs::create_dir_all(&nested).expect("create nested directory");
|
|
1290
|
+
std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(0o777))
|
|
1291
|
+
.expect("set insecure ancestor permissions");
|
|
1292
|
+
std::fs::set_permissions(&nested, std::fs::Permissions::from_mode(0o700))
|
|
1293
|
+
.expect("set nested permissions");
|
|
1294
|
+
|
|
1295
|
+
let path = nested.join("daemon.sock");
|
|
1296
|
+
let err = ensure_socket_parent(&path).expect_err("must reject");
|
|
1297
|
+
assert!(err.contains("must not be writable by group/other"));
|
|
1298
|
+
|
|
1299
|
+
std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(0o700))
|
|
1300
|
+
.expect("restore cleanup permissions");
|
|
1301
|
+
std::fs::remove_dir_all(&root).expect("cleanup");
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
#[test]
|
|
1305
|
+
#[cfg(unix)]
|
|
1306
|
+
fn assert_root_owned_daemon_socket_path_rejects_symlink() {
|
|
1307
|
+
use std::os::unix::fs::symlink;
|
|
1308
|
+
|
|
1309
|
+
let root = std::env::temp_dir().join(format!(
|
|
1310
|
+
"wlfi-client-socket-symlink-{}-{}",
|
|
1311
|
+
std::process::id(),
|
|
1312
|
+
uuid::Uuid::new_v4().simple()
|
|
1313
|
+
));
|
|
1314
|
+
std::fs::create_dir_all(&root).expect("create root directory");
|
|
1315
|
+
|
|
1316
|
+
let target = root.join("daemon.sock.target");
|
|
1317
|
+
std::fs::write(&target, "not a socket").expect("write target");
|
|
1318
|
+
let linked = root.join("daemon.sock");
|
|
1319
|
+
symlink(&target, &linked).expect("create symlink");
|
|
1320
|
+
|
|
1321
|
+
let err = assert_root_owned_daemon_socket_path(&linked).expect_err("must reject");
|
|
1322
|
+
assert!(err.contains("must not be a symlink"));
|
|
1323
|
+
|
|
1324
|
+
std::fs::remove_dir_all(&root).expect("cleanup");
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
#[test]
|
|
1328
|
+
#[cfg(unix)]
|
|
1329
|
+
fn assert_root_owned_daemon_socket_path_rejects_non_socket_files() {
|
|
1330
|
+
let root = std::env::temp_dir().join(format!(
|
|
1331
|
+
"wlfi-client-socket-file-{}-{}",
|
|
1332
|
+
std::process::id(),
|
|
1333
|
+
uuid::Uuid::new_v4().simple()
|
|
1334
|
+
));
|
|
1335
|
+
std::fs::create_dir_all(&root).expect("create root directory");
|
|
1336
|
+
|
|
1337
|
+
let file = root.join("daemon.sock");
|
|
1338
|
+
std::fs::write(&file, "not a socket").expect("write file");
|
|
1339
|
+
|
|
1340
|
+
let err = assert_root_owned_daemon_socket_path(&file).expect_err("must reject");
|
|
1341
|
+
assert!(err.contains("must be a unix socket"));
|
|
1342
|
+
|
|
1343
|
+
std::fs::remove_dir_all(&root).expect("cleanup");
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
1347
|
+
async fn unix_round_trip_for_issue_lease() {
|
|
1348
|
+
let socket_path = unique_socket_path("lease");
|
|
1349
|
+
let daemon = Arc::new(
|
|
1350
|
+
InMemoryDaemon::new(
|
|
1351
|
+
"vault-password",
|
|
1352
|
+
SoftwareSignerBackend::default(),
|
|
1353
|
+
DaemonConfig::default(),
|
|
1354
|
+
)
|
|
1355
|
+
.expect("daemon"),
|
|
1356
|
+
);
|
|
1357
|
+
let server = UnixDaemonServer::bind(
|
|
1358
|
+
socket_path.clone(),
|
|
1359
|
+
singleton_allowed_set(nix::unistd::geteuid().as_raw()),
|
|
1360
|
+
)
|
|
1361
|
+
.await
|
|
1362
|
+
.expect("server");
|
|
1363
|
+
let server_task = tokio::spawn({
|
|
1364
|
+
let daemon = daemon.clone();
|
|
1365
|
+
async move {
|
|
1366
|
+
server
|
|
1367
|
+
.run_until_shutdown(daemon, async {
|
|
1368
|
+
std::future::pending::<()>().await;
|
|
1369
|
+
})
|
|
1370
|
+
.await
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
1375
|
+
let client = UnixDaemonClient::new(socket_path.clone(), Duration::from_secs(2));
|
|
1376
|
+
let lease = client.issue_lease("vault-password").await.expect("lease");
|
|
1377
|
+
assert_eq!(lease.lease_id.get_version_num(), 4);
|
|
1378
|
+
|
|
1379
|
+
server_task.abort();
|
|
1380
|
+
let _ = server_task.await;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
1384
|
+
async fn client_rejects_unexpected_daemon_euid() {
|
|
1385
|
+
let socket_path = unique_socket_path("peer-euid");
|
|
1386
|
+
let daemon = Arc::new(
|
|
1387
|
+
InMemoryDaemon::new(
|
|
1388
|
+
"vault-password",
|
|
1389
|
+
SoftwareSignerBackend::default(),
|
|
1390
|
+
DaemonConfig::default(),
|
|
1391
|
+
)
|
|
1392
|
+
.expect("daemon"),
|
|
1393
|
+
);
|
|
1394
|
+
let allowed_euid = nix::unistd::geteuid().as_raw();
|
|
1395
|
+
let server =
|
|
1396
|
+
UnixDaemonServer::bind(socket_path.clone(), singleton_allowed_set(allowed_euid))
|
|
1397
|
+
.await
|
|
1398
|
+
.expect("server");
|
|
1399
|
+
let server_task = tokio::spawn({
|
|
1400
|
+
let daemon = daemon.clone();
|
|
1401
|
+
async move {
|
|
1402
|
+
server
|
|
1403
|
+
.run_until_shutdown(daemon, async {
|
|
1404
|
+
std::future::pending::<()>().await;
|
|
1405
|
+
})
|
|
1406
|
+
.await
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
1411
|
+
let mismatched_expected_euid = allowed_euid.saturating_add(1);
|
|
1412
|
+
let client = UnixDaemonClient::new_with_expected_server_euid(
|
|
1413
|
+
socket_path.clone(),
|
|
1414
|
+
Duration::from_secs(2),
|
|
1415
|
+
mismatched_expected_euid,
|
|
1416
|
+
);
|
|
1417
|
+
let err = client
|
|
1418
|
+
.issue_lease("vault-password")
|
|
1419
|
+
.await
|
|
1420
|
+
.expect_err("must fail");
|
|
1421
|
+
assert!(
|
|
1422
|
+
err.to_string().contains("unauthorized peer euid"),
|
|
1423
|
+
"unexpected error: {err}"
|
|
1424
|
+
);
|
|
1425
|
+
|
|
1426
|
+
server_task.abort();
|
|
1427
|
+
let _ = server_task.await;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
#[test]
|
|
1431
|
+
fn rpc_access_level_classifies_admin_and_agent_requests() {
|
|
1432
|
+
let admin_request = vault_daemon::DaemonRpcRequest::IssueLease {
|
|
1433
|
+
vault_password: "vault-password".to_string(),
|
|
1434
|
+
};
|
|
1435
|
+
let agent_request = vault_daemon::DaemonRpcRequest::EvaluateForAgent {
|
|
1436
|
+
request: dummy_agent_request(),
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
assert_eq!(rpc_access_level(&admin_request), RpcAccessLevel::Admin);
|
|
1440
|
+
assert_eq!(rpc_access_level(&agent_request), RpcAccessLevel::Agent);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
#[test]
|
|
1444
|
+
fn socket_mode_is_private_only_when_both_allowlists_are_root_only() {
|
|
1445
|
+
assert_eq!(
|
|
1446
|
+
socket_mode_for_allowed_peer_euids(
|
|
1447
|
+
&singleton_allowed_set(0),
|
|
1448
|
+
&singleton_allowed_set(0)
|
|
1449
|
+
),
|
|
1450
|
+
0o600
|
|
1451
|
+
);
|
|
1452
|
+
|
|
1453
|
+
let current_euid = nix::unistd::geteuid().as_raw();
|
|
1454
|
+
assert_eq!(
|
|
1455
|
+
socket_mode_for_allowed_peer_euids(
|
|
1456
|
+
&singleton_allowed_set(0),
|
|
1457
|
+
&singleton_allowed_set(current_euid)
|
|
1458
|
+
),
|
|
1459
|
+
0o666
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
#[test]
|
|
1464
|
+
fn combined_allowed_peer_euids_deduplicates_split_allowlists() {
|
|
1465
|
+
let current_euid = nix::unistd::geteuid().as_raw();
|
|
1466
|
+
let combined = combined_allowed_peer_euids(
|
|
1467
|
+
&BTreeSet::from([0, current_euid]),
|
|
1468
|
+
&BTreeSet::from([current_euid, current_euid.saturating_add(1)]),
|
|
1469
|
+
);
|
|
1470
|
+
|
|
1471
|
+
assert_eq!(
|
|
1472
|
+
combined,
|
|
1473
|
+
BTreeSet::from([0, current_euid, current_euid.saturating_add(1)])
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
#[tokio::test(flavor = "current_thread")]
|
|
1478
|
+
async fn handle_connection_rejects_admin_requests_from_agent_only_peers() {
|
|
1479
|
+
let daemon = Arc::new(
|
|
1480
|
+
InMemoryDaemon::new(
|
|
1481
|
+
"vault-password",
|
|
1482
|
+
SoftwareSignerBackend::default(),
|
|
1483
|
+
DaemonConfig::default(),
|
|
1484
|
+
)
|
|
1485
|
+
.expect("daemon"),
|
|
1486
|
+
);
|
|
1487
|
+
let current_euid = nix::unistd::geteuid().as_raw();
|
|
1488
|
+
let (mut client_stream, server_stream) = UnixStream::pair().expect("stream pair");
|
|
1489
|
+
let wire_request = WireRequest {
|
|
1490
|
+
body_json: serde_json::to_string(&vault_daemon::DaemonRpcRequest::IssueLease {
|
|
1491
|
+
vault_password: "vault-password".to_string(),
|
|
1492
|
+
})
|
|
1493
|
+
.expect("serialize request"),
|
|
1494
|
+
};
|
|
1495
|
+
write_frame(&mut client_stream, &wire_request, Duration::from_secs(2))
|
|
1496
|
+
.await
|
|
1497
|
+
.expect("write request");
|
|
1498
|
+
|
|
1499
|
+
let err = handle_connection(
|
|
1500
|
+
server_stream,
|
|
1501
|
+
daemon,
|
|
1502
|
+
singleton_allowed_set(current_euid.saturating_add(1)),
|
|
1503
|
+
singleton_allowed_set(current_euid),
|
|
1504
|
+
)
|
|
1505
|
+
.await
|
|
1506
|
+
.expect_err("must reject admin request");
|
|
1507
|
+
|
|
1508
|
+
assert!(
|
|
1509
|
+
matches!(err, UnixTransportError::UnauthorizedPeerEuid { actual, .. } if actual == current_euid)
|
|
1510
|
+
);
|
|
1511
|
+
assert!(
|
|
1512
|
+
read_frame::<WireResponse>(&mut client_stream, Duration::from_millis(50))
|
|
1513
|
+
.await
|
|
1514
|
+
.is_err()
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
#[tokio::test(flavor = "current_thread")]
|
|
1519
|
+
async fn handle_connection_rejects_globally_unauthorized_peer_without_frame_read() {
|
|
1520
|
+
let daemon = Arc::new(
|
|
1521
|
+
InMemoryDaemon::new(
|
|
1522
|
+
"vault-password",
|
|
1523
|
+
SoftwareSignerBackend::default(),
|
|
1524
|
+
DaemonConfig::default(),
|
|
1525
|
+
)
|
|
1526
|
+
.expect("daemon"),
|
|
1527
|
+
);
|
|
1528
|
+
let current_euid = nix::unistd::geteuid().as_raw();
|
|
1529
|
+
let (_client_stream, server_stream) = UnixStream::pair().expect("stream pair");
|
|
1530
|
+
|
|
1531
|
+
let result = tokio::time::timeout(
|
|
1532
|
+
Duration::from_millis(200),
|
|
1533
|
+
handle_connection(
|
|
1534
|
+
server_stream,
|
|
1535
|
+
daemon,
|
|
1536
|
+
singleton_allowed_set(current_euid.saturating_add(1)),
|
|
1537
|
+
singleton_allowed_set(current_euid.saturating_add(2)),
|
|
1538
|
+
),
|
|
1539
|
+
)
|
|
1540
|
+
.await
|
|
1541
|
+
.expect("globally unauthorized peer should be rejected before read timeout");
|
|
1542
|
+
|
|
1543
|
+
let err = result.expect_err("must reject globally unauthorized peer");
|
|
1544
|
+
assert!(
|
|
1545
|
+
matches!(err, UnixTransportError::UnauthorizedPeerEuid { actual, .. } if actual == current_euid)
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
#[tokio::test(flavor = "current_thread")]
|
|
1550
|
+
async fn handle_connection_prioritizes_unauthorized_over_deserialization_for_globally_unauthorized_peer(
|
|
1551
|
+
) {
|
|
1552
|
+
let daemon = Arc::new(
|
|
1553
|
+
InMemoryDaemon::new(
|
|
1554
|
+
"vault-password",
|
|
1555
|
+
SoftwareSignerBackend::default(),
|
|
1556
|
+
DaemonConfig::default(),
|
|
1557
|
+
)
|
|
1558
|
+
.expect("daemon"),
|
|
1559
|
+
);
|
|
1560
|
+
let current_euid = nix::unistd::geteuid().as_raw();
|
|
1561
|
+
let (mut client_stream, server_stream) = UnixStream::pair().expect("stream pair");
|
|
1562
|
+
let wire_request = WireRequest {
|
|
1563
|
+
body_json: "{not-json".to_string(),
|
|
1564
|
+
};
|
|
1565
|
+
write_frame(&mut client_stream, &wire_request, Duration::from_secs(2))
|
|
1566
|
+
.await
|
|
1567
|
+
.expect("write malformed request");
|
|
1568
|
+
|
|
1569
|
+
let err = handle_connection(
|
|
1570
|
+
server_stream,
|
|
1571
|
+
daemon,
|
|
1572
|
+
singleton_allowed_set(current_euid.saturating_add(1)),
|
|
1573
|
+
singleton_allowed_set(current_euid.saturating_add(2)),
|
|
1574
|
+
)
|
|
1575
|
+
.await
|
|
1576
|
+
.expect_err("must reject globally unauthorized peer");
|
|
1577
|
+
|
|
1578
|
+
assert!(
|
|
1579
|
+
matches!(err, UnixTransportError::UnauthorizedPeerEuid { actual, .. } if actual == current_euid)
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
#[tokio::test(flavor = "current_thread")]
|
|
1584
|
+
async fn handle_connection_rejects_agent_requests_from_admin_only_peers() {
|
|
1585
|
+
let daemon = Arc::new(
|
|
1586
|
+
InMemoryDaemon::new(
|
|
1587
|
+
"vault-password",
|
|
1588
|
+
SoftwareSignerBackend::default(),
|
|
1589
|
+
DaemonConfig::default(),
|
|
1590
|
+
)
|
|
1591
|
+
.expect("daemon"),
|
|
1592
|
+
);
|
|
1593
|
+
let current_euid = nix::unistd::geteuid().as_raw();
|
|
1594
|
+
let (mut client_stream, server_stream) = UnixStream::pair().expect("stream pair");
|
|
1595
|
+
let wire_request = WireRequest {
|
|
1596
|
+
body_json: serde_json::to_string(&vault_daemon::DaemonRpcRequest::EvaluateForAgent {
|
|
1597
|
+
request: dummy_agent_request(),
|
|
1598
|
+
})
|
|
1599
|
+
.expect("serialize request"),
|
|
1600
|
+
};
|
|
1601
|
+
write_frame(&mut client_stream, &wire_request, Duration::from_secs(2))
|
|
1602
|
+
.await
|
|
1603
|
+
.expect("write request");
|
|
1604
|
+
|
|
1605
|
+
let err = handle_connection(
|
|
1606
|
+
server_stream,
|
|
1607
|
+
daemon,
|
|
1608
|
+
singleton_allowed_set(current_euid),
|
|
1609
|
+
singleton_allowed_set(current_euid.saturating_add(1)),
|
|
1610
|
+
)
|
|
1611
|
+
.await
|
|
1612
|
+
.expect_err("must reject agent request");
|
|
1613
|
+
|
|
1614
|
+
assert!(
|
|
1615
|
+
matches!(err, UnixTransportError::UnauthorizedPeerEuid { actual, .. } if actual == current_euid)
|
|
1616
|
+
);
|
|
1617
|
+
assert!(
|
|
1618
|
+
read_frame::<WireResponse>(&mut client_stream, Duration::from_millis(50))
|
|
1619
|
+
.await
|
|
1620
|
+
.is_err()
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
1625
|
+
async fn bind_with_split_allowlists_keeps_root_only_socket_private() {
|
|
1626
|
+
let socket_path = unique_socket_path("split-root-only-mode");
|
|
1627
|
+
let server = UnixDaemonServer::bind_with_allowed_peer_euids(
|
|
1628
|
+
socket_path.clone(),
|
|
1629
|
+
singleton_allowed_set(0),
|
|
1630
|
+
singleton_allowed_set(0),
|
|
1631
|
+
)
|
|
1632
|
+
.await
|
|
1633
|
+
.expect("server");
|
|
1634
|
+
|
|
1635
|
+
#[cfg(unix)]
|
|
1636
|
+
assert_eq!(socket_mode(&socket_path), 0o600);
|
|
1637
|
+
|
|
1638
|
+
drop(server);
|
|
1639
|
+
}
|
|
1640
|
+
}
|