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
150 lines
8.6 KiB
Markdown
150 lines
8.6 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 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](mnemonic-derivation.md) | draft | BIP39, SLIP-0010, BIP-0032, derivation paths, key types |
|
|
| [encryption.md](encryption.md) | draft | AES-256-GCM, EncryptedData, key versioning, HD derivation (ADR-020) |
|
|
| [service.md](service.md) | draft | VaultServiceHandle lifecycle, actor dispatch, cache, error model |
|
|
| [protocol.md](protocol.md) | draft | VaultProtocol irpc messages, DerivedKey redaction, serialization |
|
|
|
|
## Applicable ADRs
|
|
|
|
| ADR | Title | Relevance |
|
|
|-----|-------|-----------|
|
|
| [003](../../decisions/003-crate-decomposition.md) | Crate Decomposition | alknet-vault's standalone position |
|
|
| [006](../../decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention | ALPN versioning pattern for potential `alknet/vault/v2` |
|
|
| [005](../../decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | VaultProtocol uses irpc directly |
|
|
| [008](../../decisions/008-secret-service-integration.md) | Vault Integration Point | CLI-embedded, capability source |
|
|
| [010](../../decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Ed25519 as default curve for TLS raw key identity |
|
|
| [014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Secret Material Flow and Capability Injection | Capabilities carry vault-derived material |
|
|
| [018](../../decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Zero alknet crate dependencies |
|
|
| [019](../../decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | The assembly layer is the sole caller |
|
|
| [020](../../decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | SLIP-0010 derivation, not PBKDF2; salt unused in v2 |
|
|
| [021](../../decisions/021-key-rotation-via-version-indexed-paths.md) | 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
|
|
|
|
1. **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.
|
|
2. **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.
|
|
3. **Zeroize everything sensitive**: The mnemonic, seed, derived private
|
|
keys, encryption keys, and cached keys all implement `Zeroize` and
|
|
`ZeroizeOnDrop`. Secret material does not linger in freed heap memory.
|
|
4. **Deterministic derivation**: The same mnemonic + passphrase + path
|
|
always produces the same key. Derivation is reproducible across runs
|
|
and across nodes.
|
|
5. **OsRng for nonces**: AES-GCM IVs and any cryptographic nonces use
|
|
`OsRng` (or equivalent CSPRNG), never `rand::random()`. IV reuse under
|
|
the same key is catastrophic for GCM.
|
|
6. **No `unwrap()` or `expect()` outside tests**: vault operations
|
|
propagate errors. A poisoned lock is recovered with
|
|
`unwrap_or_else(|e| e.into_inner())`, not `unwrap()`. 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](service.md#security-constraints) for
|
|
the full list.
|
|
|
|
- **OsRng for IVs**: AES-GCM IVs must use `OsRng`, not `rand::random()`. The
|
|
current source uses `rand::random()` — this is a known drift from the
|
|
spec and must be corrected during implementation sync.
|
|
- **Zeroized drop**: `Seed`, `Mnemonic`, `ExtendedPrivKey`,
|
|
`Secp256k1ExtendedPrivKey`, `EncryptionKey`, `CachedKey`, and
|
|
`DerivedKey` all derive `Zeroize` and `ZeroizeOnDrop`. The cache must
|
|
clear on drop, not just on explicit `lock()`.
|
|
- **No `unwrap()` outside tests**: poisoned lock recovery uses
|
|
`unwrap_or_else(|e| e.into_inner())` or explicit error propagation. The
|
|
current source uses `unwrap()` in `VaultServiceHandle` methods — this
|
|
is a known drift and must be corrected.
|
|
- **DerivedKey redaction in JSON**: `DerivedKey` serializes the
|
|
`private_key` as `"[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 that
|
|
`DerivedKey` never 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](encryption.md#security-constraints), [service.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](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](encryption.md#key-versioning), [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) |
|
|
| 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](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](service.md#security-constraints) |
|
|
|
|
## Public API
|
|
|
|
The vault re-exports its primary types from the crate root:
|
|
|
|
```rust
|
|
// 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:
|
|
|
|
```rust
|
|
#[cfg(feature = "secp256k1")]
|
|
pub mod ethereum;
|
|
``` |