Review #003 found 11 critical, 14 warning, and 6 suggestion findings after reviews #001 (governance/security) and #002 (cross-document consistency/two-way-door audit) were resolved. The theme: types and APIs that were *referenced* but never *defined*, and stale ADR sketches that didn't match the now-updated spec docs. Critical fixes (11): - C1: DerivedKey #[derive(Deserialize)] contradicted the custom Deserialize that rejects "[REDACTED]" — dropped the derive, added explicit manual Serialize/Deserialize impls (protocol.md). - C2: encrypt prose said "derived at PATHS::ENCRYPTION" but the signature takes key_version — updated to encryption_path_for_version (service.md). - C3: derive_encryption_key returned DerivedKey, derive_encryption_key _for_version returned EncryptionKey (same cache) — unified on DerivedKey, defined CachedKey (service.md). - C4: tokio vs std::sync::RwLock contradiction — specified std::sync::RwLock, dropped tokio from vault deps (ADR-018, ADR-025, service.md). - C5: Missing drift rows in vault README — added #9 (key_version ignored) and #10 (rotate not implemented). - C6: ADR-022 build_root_context and invoke() sketches omitted abort_policy (9 fields vs 10) — added the field to both sketches. - C7: Capabilities type referenced 20+ times, never defined — added struct definition to core-types.md with Clone+Send+Sync, Zeroize, sealed builder API, immutability guard. - C8: SessionOverlaySource on CallAdapter but never defined, crate violation (alknet-call can't depend on alknet-agent) — defined the trait in alknet-call (call-protocol.md), matching the IdentityProvider pattern. - C9: CompositeOperationEnv dispatch fall-through was "a two-way door" — added contains() to OperationEnv trait, made the composite probe before dispatching, eliminating the sentinel ambiguity. - C10: No API for Layer 2 (connection overlay) registration, CallConnection undefined — defined CallConnection struct + register_imported() API (call-protocol.md). - C11: with_local signature diverged between two examples (4 args vs 5) — added capabilities as the 5th arg, made both examples consistent. Warning fixes (14): - W1: invoke_with_policy restructured as required method, invoke gets a default impl delegating to it — eliminates duplication across impls. - W2: CachedKey defined (service.md). - W3: EncryptionKey constructor/glue specified, added to re-export list. - W4: Secp256k1ExtendedPrivKey defined, derive_ethereum_key glue shown. - W5: encryption_path_for_version rejects version < 2 (v1 is TS PBKDF2). - W6: Wire payload schemas for all event types + ResponseEnvelope → EventEnvelope conversion table (call-protocol.md). - W7: Timeout section — deadline on OperationContext, composed calls inherit parent's deadline, CallAdapter::with_timeout(). - W8: Request ID generation spec — UUID v4 for composed calls, wire ID vs internal ID relationship for abort cascade. - W9: unlock_new already-unlocked behavior specified (returns AlreadyUnlocked). - W10: KeyType Serialize/Deserialize justification corrected (stale irpc reference removed). - W11: OperationProvenance and CompositionAuthority defined inline in operation-registry.md (were only in ADR-022). - W12: encrypt/decrypt free functions marked pub(crate), relationship to VaultServiceHandle methods stated. - W13: rotate signature removed from encryption.md (it's a VaultServiceHandle method, not a free function). - W14: CallAdapter::new() + with_session_source() + with_timeout() constructors shown. Suggestion fixes (6): Seed: Clone note, VaultServiceInner invariant, ExtendedPrivKey accessor signatures, CURRENT_KEY_VERSION location, ADR-018 stale actor text, derivation helpers re-export note.
11 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-23 |
alknet-vault
Local key vault: BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, BIP-0032 secp256k1 derivation (feature-gated), and AES-256-GCM encryption. Holds the master seed — the root of trust for all derived keys and encrypted credentials in the alknet system.
What This Crate Is
alknet-vault is a standalone crate with zero alknet crate dependencies
(ADR-018) and zero RPC framework dependencies (ADR-025). It provides the
cryptographic primitives and runtime API for managing the root of trust.
The CLI binary (the alknet crate) is the sole component that talks to the
vault directly (ADR-019) — handlers receive derived/decrypted material
through capabilities, never through a vault reference.
The vault is not a network service. It has no ALPN, no
ProtocolHandler implementation, no operations registered in the call
protocol (ADR-008, ADR-014), and no remote dispatch capability (ADR-025).
The vault is local-only by construction — direct method calls on
VaultServiceHandle, no actor, no message enum, no wire format. The master
seed and derived private keys never cross the network.
Documents
| Document | Status | Description |
|---|---|---|
| mnemonic-derivation.md | draft | BIP39, SLIP-0010, BIP-0032, derivation paths, key types |
| encryption.md | draft | AES-256-GCM, EncryptedData, key versioning, HD derivation (ADR-020) |
| service.md | draft | VaultServiceHandle lifecycle, direct dispatch, cache, error model |
| protocol.md | draft | DerivedKey redaction, KeyType, serialization behavior |
Applicable ADRs
| ADR | Title | Relevance |
|---|---|---|
| 003 | Crate Decomposition | alknet-vault's standalone position |
| 008 | Vault Integration Point | CLI-embedded, capability source |
| 010 | ALPN Router and Endpoint | Ed25519 as default curve for TLS raw key identity |
| 014 | Secret Material Flow and Capability Injection | Capabilities carry vault-derived material |
| 018 | Vault as Standalone Crate | Zero alknet crate dependencies |
| 019 | Vault Assembly-Layer-Only Access | The assembly layer is the sole caller |
| 020 | HD Derivation for Encryption Keys | SLIP-0010 derivation, not PBKDF2; salt unused in v2 |
| 021 | Key Rotation via Version-Indexed Paths | Version-indexed paths; rotate re-encrypts |
| 025 | Vault Local-Only Dispatch | Dropped irpc; direct method calls; local-only by construction |
| 026 | Vault Key Model — HD Derivation | HD derivation from BIP39 seed; 74' coin type; AES-256-GCM |
Relevant Open Questions
| OQ | Title | Status | Relevance |
|---|---|---|---|
| OQ-20 | Encryption key derivation | resolved (ADR-020) | HD derivation from seed; salt field unused in v2 |
| OQ-21 | Remote vault access | resolved (ADR-025) | Vault is local-only by construction; remote access requires a separate vault-server crate with its own ADR |
| OQ-22 | Key rotation mechanism | resolved (ADR-021) | Version-indexed paths; rotate method |
Key Design Principles
- Standalone: The vault depends on no alknet crate and no RPC framework. It defines its own types and errors. External crates depend on the vault; the vault depends on nothing in alknet.
- Assembly-layer only: The vault's API is consumed by the CLI binary, not by handlers. Handlers receive material through capabilities (ADR-014). The vault is not on the wire.
- Local-only by construction: The vault has no remote dispatch
capability. Direct method calls on
VaultServiceHandle— no actor, no message enum, no wire format (ADR-025). Remote access, if ever needed, requires a separate crate with its own ADR. - Zeroize everything sensitive: The mnemonic, seed, derived private
keys, encryption keys, and cached keys all implement
ZeroizeandZeroizeOnDrop. Secret material does not linger in freed heap memory. - Deterministic derivation: The same mnemonic + passphrase + path always produces the same key. Derivation is reproducible across runs and across nodes.
- OsRng for nonces: AES-GCM IVs and any cryptographic nonces use
OsRng(or equivalent CSPRNG), neverrand::random(). IV reuse under the same key is catastrophic for GCM. - No
unwrap()orexpect()outside tests: vault operations propagate errors. A poisoned lock is recovered withunwrap_or_else(|e| e.into_inner()), notunwrap(). A panic in one vault operation must not brick the vault for all other operations.
Security Constraints
These are security-critical implementation requirements, not architectural decisions (the architecture is locked by the ADRs above). They are documented here so implementation agents don't miss them. See service.md → Security Constraints for the full list.
- OsRng for IVs: AES-GCM IVs must use
OsRng, notrand::random(). The current source usesrand::random()— this is a known drift from the spec and must be corrected during implementation sync. - Zeroized drop:
Seed,Mnemonic,ExtendedPrivKey,Secp256k1ExtendedPrivKey,EncryptionKey,CachedKey, andDerivedKeyall deriveZeroizeandZeroizeOnDrop. The cache must clear on drop, not just on explicitlock(). - No
unwrap()outside tests: poisoned lock recovery usesunwrap_or_else(|e| e.into_inner())or explicit error propagation. The current source usesunwrap()inVaultServiceHandlemethods — this is a known drift and must be corrected. - DerivedKey redaction in serialization:
DerivedKeyserializes theprivate_keyas"[REDACTED]"in all formats (ADR-025 dropped the postcard/remote path that previously preserved bytes in binary formats). Deserialization rejects"[REDACTED]"with an error (resolves review #002 W8). The redaction is a defense-in-depth measure for logging safety, not the primary control — the primary control is thatDerivedKeynever crosses the call protocol wire (ADR-014).
Known Source Drift
The vault crate carries over source from the POC. The following items are known divergences between the current source and the spec. All must be corrected during implementation sync. This table is the single source of truth for drift tracking — if an item is fixed in source, update this table.
| # | Item | Current source behavior | Target behavior (per spec) | Source location | Spec reference |
|---|---|---|---|---|---|
| 1 | IV generation | rand::random() |
OsRng (CSPRNG) |
encryption.rs L133 |
encryption.md → Security Constraints, service.md → Security Constraints |
| 2 | RwLock unwrap() |
unwrap() on every RwLock acquisition (L142, 161, 182, 191, 196, 227, 264, 307, 340, 367) |
unwrap_or_else(|e| e.into_inner()) for poisoned lock recovery |
service.rs (see line numbers) |
service.md → Security Constraints |
| 3 | CURRENT_KEY_VERSION |
1 (HD-derived, but v1 is reserved for TS PBKDF2 legacy per ADR-020) |
2 (HD-derived, per ADR-020) |
encryption.rs |
encryption.md → Key Versioning, ADR-020 |
| 4 | irpc dependency | VaultProtocol enum with #[rpc_requests], VaultServiceActor, Client<VaultProtocol>, irpc/postcard deps |
Remove entirely — direct method calls on VaultServiceHandle (ADR-025) |
protocol.rs, service.rs, Cargo.toml |
ADR-025 |
| 5 | DerivedKey dual serialization |
JSON redacts, postcard preserves bytes | Always redact on serialize; reject "[REDACTED]" on deserialize with error (ADR-025, resolves W8) |
protocol.rs |
protocol.md → Serialization Redaction, ADR-025 |
| 6 | HashMap::clear zeroization |
KeyCache::clear() removes entries and relies on CachedKey's Drop impl for zeroization |
Verify HashMap::clear() actually drops values (it does, but worth a test) |
cache.rs |
service.md → Security Constraints |
| 7 | derive_password / site_password_path |
derive_password, derive_password_string, site_password_path methods exist |
Remove entirely — password-manager pattern not relevant to RPC system's vault (ADR-025, resolves C9) | service.rs, mnemonic-derivation.rs |
ADR-025 |
| 8 | unlock_new return type |
Returns String (not zeroized on drop) |
Return Zeroizing<String> — the mnemonic is the root of trust and must not linger in freed memory (resolves W7) |
service.rs |
service.md → unlock_new |
| 9 | key_version ignored in encrypt/decrypt |
encrypt/decrypt always derive at PATHS::ENCRYPTION regardless of key_version |
Derive at encryption_path_for_version(key_version) — encrypt stamps the passed version, decrypt selects the key by the blob's version (ADR-021) |
service.rs |
service.md → encrypt, ADR-021 |
| 10 | rotate not implemented |
No rotate method exists |
Implement rotate(encrypted, to_version) — decrypt with old version's key, re-encrypt with new version's key (ADR-021) |
service.rs |
service.md → rotate, ADR-021 |
Public API
The vault re-exports its primary types from the crate root:
// Mnemonic and seed
pub use mnemonic::{Language, Mnemonic, Seed};
// Derivation
pub use derivation::{DerivationError, ExtendedPrivKey, PATHS};
// Derivation helpers (derive_path_from_seed, parse_derivation_path,
// device_path, encryption_path_for_version) are accessible as
// alknet_vault::derivation::* — not re-exported at crate root to avoid
// clutter, but fully public.
// Encryption
pub use encryption::{EncryptedData, EncryptionError, EncryptionKey};
pub use encryption::CURRENT_KEY_VERSION;
// Key types (DerivedKey, KeyType)
pub use protocol::{DerivedKey, KeyType};
// Service (runtime)
pub use service::{VaultServiceError, VaultServiceHandle};
// Cache
pub use cache::CacheConfig;
The secp256k1 feature flag gates Ethereum (BIP-0032) derivation:
#[cfg(feature = "secp256k1")]
pub mod ethereum;