Governance (Tier 2):
- Advance ADR-022 and ADR-023 from Proposed to Accepted (specs already
depend on their types as source of truth)
- Amend ADR-015: mark Decision 3 and Assumption 6 as superseded by ADR-022;
update handler_identity type to CompositionAuthority
- Amend ADR-002: note handle() signature revised by ADR-007 (BiStream → Connection)
- Amend ADR-004: note 'enrich/replace' AuthContext language superseded by
ADR-011's immutability model; update to describe set_identity on Connection
- Update main README ADR table to show ADR-022/023 as Accepted
Spec-ADR consistency (Tier 3):
- Add abort_policy: AbortPolicy field to OperationContext struct (ADR-016
Decision 6 mandated this but the spec omitted it)
- Define AbortPolicy enum (AbortDependents | ContinueRunning) with Default impl
- Add abort_policy to build_root_context and LocalOperationEnv::invoke()
- Define the OperationEnv trait explicitly with invoke() and
invoke_with_policy() methods (was referenced as 'must remain a trait'
but never defined)
- Specify From<StreamError> for HandlerError impl with exact variant mapping
- Add Connection::from_quinn() / from_iroh() constructors (was referenced
as Connection::new() but never defined)
- Remove undefined CertAuthorityEntry placeholder from AuthPolicy v1 (will
be added additively when alknet-ssh lands)
- Fix config.md key-differences table: rate limits are in DynamicConfig,
not StaticConfig
Mechanical fixes (Tier 1):
- overview.md: 'closes the QUIC stream' → 'closes the connection' (stale
from pre-ADR-007 model)
- overview.md: OQ-04 entry updated from stale 'defer to implementation'
to 'resolved: static at startup'
- mnemonic-derivation.md: remove duplicate helper functions block (incomplete
first copy, complete second copy)
- ADR-003: add iroh (feature-gated) to alknet-core dependency list, added
by ADR-010
- ADR-021: fix ambiguous 'W1 drift issue from the vault review' cross-reference
- ADR-022: rephrase FromCall 'leaf locally' to 'leaf in the local registry'
- ADR-017: add error_schemas to from_call mirror list and services/schema
step (inconsistency with ADR-023)
- ADR-016: fix self-referential citation ('ADR-016 Assumption 5' → 'Assumption 5')
- Add ScopedOperationEnv::empty(), allows(), new() and
CompositionAuthority::none(), new() impl blocks (referenced but undefined)
- Add call.completed clarification for non-subscription calls
- Add services/schema leading-slash normalization note
- Crate README ADR tables: add missing ADR-013 (call), ADR-015 (core),
ADR-006 + ADR-010 (vault)
- Vault README: add consolidated 'Known Source Drift' table tracking all
four drift items (OsRng, unwrap, CURRENT_KEY_VERSION, spawn bug) in one
place, including the two previously missing from README
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-22-19 |
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). 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, and no operations registered in the call
protocol (ADR-008, ADR-014). 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, actor dispatch, cache, error model |
| protocol.md | draft | VaultProtocol irpc messages, DerivedKey redaction, serialization |
Applicable ADRs
| ADR | Title | Relevance |
|---|---|---|
| 003 | Crate Decomposition | alknet-vault's standalone position |
| 006 | ALPN String Convention | ALPN versioning pattern for potential alknet/vault/v2 |
| 005 | irpc as Call Protocol Foundation | VaultProtocol uses irpc directly |
| 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 |
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 | deferred | Protocol is remote-capable by construction; enabling = server-setup change with auth-wrapping handler; Unlock/Lock local-only |
| OQ-22 | Key rotation mechanism | resolved (ADR-021) | Version-indexed paths; rotate method |
Key Design Principles
- Standalone: The vault depends on no alknet crate. It defines its own types, errors, and protocol. 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.
- 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 JSON:
DerivedKeyserializes theprivate_keyas"[REDACTED]"in human-readable formats (JSON) and as raw bytes in binary formats (postcard). The redaction is a defense-in- depth measure, 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 | spawn() return value |
Returns a fresh, unspawned VaultServiceActor as the second tuple element (the spawned actor is consumed by run) |
Either drop the second return value (return only Client<VaultProtocol>) or restructure so the returned actor is the one that was spawned |
service.rs VaultServiceActor::spawn() |
service.md → Actor Dispatch |
| 5 | 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 |
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};
// Encryption
pub use encryption::{EncryptedData, EncryptionError};
// Protocol (irpc messages)
pub use protocol::{DerivedKey, KeyType, VaultMessage, VaultProtocol};
// Service (runtime)
pub use service::{VaultService, VaultServiceActor, VaultServiceError, VaultServiceHandle};
// Cache
pub use cache::CacheConfig;
The secp256k1 feature flag gates Ethereum (BIP-0032) derivation:
#[cfg(feature = "secp256k1")]
pub mod ethereum;