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