The password-manager pattern (deterministic per-site passwords from HD
derivation) is not relevant to an RPC system's vault. Handlers call APIs
(using API keys, OAuth tokens, mTLS), not websites with passwords. The
vault is for cryptographic key derivation and credential encryption.
Removes:
- derive_password, derive_password_string from service.md
- site_password_path from mnemonic-derivation.md
- m/74'/1'/0'/{hash}' path from PATHS module and path semantics table
- derive_password row from the cache table
Resolves review #002 C9 (site_password_path hash mapping underspecified)
by removing the feature rather than specifying the non-standard
string→u32 mapping and Ed25519-as-password-entropy construction.
If deterministic password generation is ever needed (browser-automation
edge case), it can be re-added — the cost is near-zero. Removing it now
eliminates permanent API surface inherited from a prior project's
password-manager pattern.
10 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-22-19 |
Mnemonic and Key Derivation
BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, BIP-0032 secp256k1 derivation (feature-gated), and the derivation path constants that alknet uses.
What
The vault derives keys from a single root: a BIP39 mnemonic. From one mnemonic, all self-generated secrets are derived on demand via hierarchical deterministic (HD) derivation. This is the same model as cryptocurrency wallets — one seed phrase, many derived keys.
Two derivation schemes are supported:
| Scheme | Curve | Standard | Paths | Feature |
|---|---|---|---|---|
| SLIP-0010 | Ed25519 | HMAC-SHA512 with "ed25519 seed" |
Hardened only | default |
| BIP-0032 | secp256k1 | HMAC-SHA512 with "Bitcoin seed" |
Hardened + unhardened | secp256k1 |
Ed25519 is the default — it's what alknet's TLS identity (ADR-010), SSH
host keys, and signing keys use. secp256k1 is feature-gated for Ethereum
signing (the standard Ethereum path m/44'/60'/0'/0/0 requires
unhardened indices, which SLIP-0010 cannot handle).
Why HD Derivation
HD derivation lets one seed produce an unlimited number of keys at deterministic paths. This means:
- No key storage: keys are derived on demand, not stored. The vault caches derived keys for performance, but the cache is rebuildable from the seed.
- Reproducible across nodes: the same mnemonic on a different node produces the same keys. A backup node derives the same identity key.
- Domain separation: different paths produce different keys. The identity key, SSH host key, encryption key, and signing keys are all cryptographically independent despite coming from one seed.
- Auditable derivation: the path records what a key is for.
m/74'/0'/0'/0'is the identity key;m/74'/0'/1'/0'is the SSH host key. The path is the documentation.
BIP39 Mnemonic
The root of trust is a BIP39 mnemonic seed phrase. The vault generates, validates, and derives seeds from mnemonics.
pub struct Mnemonic {
phrase: String, // zeroized on drop
}
impl Mnemonic {
pub fn generate(word_count: usize) -> Result<Self, MnemonicError>;
pub fn from_phrase(phrase: &str, language: Language) -> Result<Self, MnemonicError>;
pub fn to_seed(&self, passphrase: Option<&str>) -> Seed;
pub fn phrase(&self) -> &str;
}
generate(word_count): Generate a new random mnemonic. Supported word counts: 12, 15, 18, 21, 24. The mnemonic is the root of trust — store it securely.from_phrase(phrase, language): Restore from an existing phrase. Validates against the BIP39 word list and checksum.to_seed(passphrase): Derive the 64-byte master seed. The passphrase is the optional BIP39 password extension (the "25th word"). Different passphrases produce different seeds.phrase(): Return the phrase string. Handle with care — this is the root of trust.
Mnemonic implements Zeroize and Drop — the phrase is zeroized
before deallocation. Only English is supported (matching the BIP39
reference and the majority of wallet software).
Seed
#[derive(Clone, Zeroize)]
#[zeroize(drop)]
pub struct Seed {
bytes: Vec<u8>, // 64 bytes, zeroized on drop
}
The 64-byte seed from which all HD keys are derived. Zeroized on drop. This is the input to SLIP-0010 / BIP-0032 master key derivation.
SLIP-0010 Ed25519 Derivation
The default derivation scheme. SLIP-0010 specifies Ed25519 HD key
derivation using HMAC-SHA512 with the key "ed25519 seed".
pub fn derive_path_from_seed(seed: &[u8], path: &str) -> Result<ExtendedPrivKey, DerivationError>;
Master key derivation
The master key is derived from the seed via HMAC-SHA512:
HMAC-SHA512(key = "ed25519 seed", data = seed)
→ first 32 bytes: private key (kL)
→ next 32 bytes: chain code
The ed25519-bip32 crate handles the extended key format (kL || kR ||
chain code). The vault extracts the first 32 bytes as the private key and
the public key (32 bytes) via XPrv::public().
Child derivation
SLIP-0010 Ed25519 supports hardened child derivation only. Every child
index must have the ' (or h) suffix, meaning index + 0x80000000.
Unhardened indices are rejected by the derivation logic (Ed25519 cannot
support them because public key derivation is not possible without the
private key).
Path parsing
pub fn parse_derivation_path(path: &str) -> Result<Vec<u32>, DerivationError>;
Parses paths like m/74'/0'/0'/0' into child indices. The m prefix is
required. Hardened indices have ' or h suffix; unhardened indices are
allowed in the parser (for BIP-0032 paths) but Ed25519 derivation will
fail on them.
ExtendedPrivKey
#[derive(Clone, Zeroize)]
#[zeroize(drop)]
pub struct ExtendedPrivKey {
private_key: Vec<u8>, // 32 bytes
public_key: Vec<u8>, // 32 bytes
chain_code: Vec<u8>, // 32 bytes
path: String, // the path that produced this key
}
The result of SLIP-0010 derivation. Zeroized on drop. Accessors return slices — the caller copies what it needs.
BIP-0032 secp256k1 Derivation (Ethereum)
Feature-gated behind secp256k1. Implements BIP-0032 HD key derivation for
the secp256k1 curve, used for Ethereum signing keys.
#[cfg(feature = "secp256k1")]
pub fn derive_secp256k1_path(seed: &[u8], path: &str) -> Result<Secp256k1ExtendedPrivKey, DerivationError>;
Unlike SLIP-0010 (Ed25519), BIP-0032 supports both hardened and
unhardened child derivation. The standard Ethereum path
m/44'/60'/0'/0/0 uses unhardened indices for the last two levels.
Why a separate module
SLIP-0010 and BIP-0032 differ in:
| Aspect | SLIP-0010 (Ed25519) | BIP-0032 (secp256k1) |
|---|---|---|
| HMAC key | "ed25519 seed" |
"Bitcoin seed" |
| Child derivation | Hardened only | Hardened + unhardened |
| Public key size | 32 bytes | 33 bytes (compressed) |
| Public derivation | Not possible | Possible (unhardened) |
The secp256k1 crate is a heavy dependency (it includes a C library for
curve operations). Feature-gating it keeps the default vault lightweight —
nodes that don't need Ethereum signing don't pay the cost.
When the feature is disabled, derive_ethereum_key returns
VaultServiceError::UnsupportedKeyType.
Derivation Paths
alknet reserves the 74' coin type (unallocated per SLIP-0044) for its
keys. Well-known paths are constants in the PATHS module:
pub mod PATHS {
pub const IDENTITY: &str = "m/74'/0'/0'/0'"; // Primary identity keypair
pub const DEVICE_PREFIX: &str = "m/74'/0'/0'"; // Worker/device identity prefix
pub const SSH_HOST: &str = "m/74'/0'/1'/0'"; // SSH host key
pub const ENCRYPTION: &str = "m/74'/2'/0'/0'"; // AES-256-GCM encryption key
pub const ETHEREUM: &str = "m/44'/60'/0'/0/0"; // Ethereum signing key (secp256k1)
}
Helper functions construct parameterized paths:
pub fn device_path(index: u32) -> String; // m/74'/0'/0'/{index}'
pub fn encryption_path_for_version(version: u32) -> String; // m/74'/2'/0'/{version-2}'
Path semantics
| Path | Purpose | Key type | Used by |
|---|---|---|---|
m/74'/0'/0'/0' |
Primary node identity (Ed25519) | Ed25519 | TLS raw key (ADR-010), node identity |
m/74'/0'/0'/{n}' |
Worker/device identity | Ed25519 | Multi-device nodes, workers |
m/74'/0'/1'/0' |
SSH host key | Ed25519 | SSH handler |
m/74'/2'/0'/0' |
Encryption key for external credentials | AES-256-GCM | Credential encryption (v2, see encryption.md) |
m/44'/60'/0'/0/0 |
Ethereum signing key | secp256k1 | Ethereum signing (feature-gated) |
encryption_path_for_version maps a key version to its derivation path
(ADR-021). v2 (current) maps to m/74'/2'/0'/0' (which is PATHS::ENCRYPTION);
v3 maps to m/74'/2'/0'/1'; etc. This is the rotation mechanism — each
version gets a cryptographically independent key from the same seed.
KeyType tags DerivedKey (see protocol.md) and
CachedKey (see service.md) so consumers know what they
received without inspecting byte lengths.
Determinism
Derivation is deterministic: the same mnemonic + passphrase + path
always produces the same key. This is verified by regression tests in
tests/test_vectors.rs against the BIP39 "abandon...about" test vector.
Passphrase sensitivity
Different passphrases produce different seeds and therefore different keys. The passphrase is a legitimate access-control mechanism: two operators with the same mnemonic but different passphrases get different keysets. The vault does not enforce a passphrase policy — that's an assembly-layer concern.
Design Decisions
| Decision | ADR | Summary |
|---|---|---|
| Vault is standalone | ADR-018 | Zero alknet crate dependencies |
| HD derivation (not stored keys) | — | One seed, many keys, no key storage |
74' coin type reserved for alknet |
— | SLIP-0044 unallocated; alknet namespace |
| secp256k1 feature-gated | — | Heavy dep; only needed for Ethereum |
| Hardened-only for Ed25519 | SLIP-0010 | Ed25519 cannot do public derivation |
Open Questions
See open-questions.md for full details.
- OQ-20 (resolved by ADR-020): Encryption key derivation — HD derivation from seed, not PBKDF2. The salt field is unused in v2. See encryption.md.
References
- BIP39 — mnemonic seed phrases
- SLIP-0010 — Ed25519 HD derivation
- BIP-0032 — secp256k1 HD derivation
- SLIP-0044 — registered coin types (74' is unallocated)
- Implementation:
crates/alknet-vault/src/mnemonic.rs,crates/alknet-vault/src/derivation.rs,crates/alknet-vault/src/ethereum.rs - Test vectors:
crates/alknet-vault/tests/test_vectors.rs