Files
alknet/docs/architecture/secret-service.md
glm-5.1 d3633b7839 docs: complete Phase 0 architecture — spec updates, review fixes, and link portability
Update four existing specs (overview, server, napi-and-pubsub, call-protocol) to
reflect Phase 0 decisions: three-layer model, IdentityProvider, ForwardingPolicy,
OperationEnv, static/dynamic config split. Review all 9 Phase 0a ADRs (026-034)
for consistency. Fix 4 critical issues from architecture review: missing OQ-SVC-05
in open-questions.md, deprecated hub terminology, undefined AuthService and noq
terms. Replace inline OQ text with cross-references per format rules. Add
ConfigServiceImpl definition to configuration.md. Port absolute workspace paths
to project-relative links by copying referenced docs (feasibility, certbot,
fail2ban, event_source_types) into docs/research/.
2026-06-07 11:27:52 +00:00

6.9 KiB

status, last_updated
status last_updated
draft 2026-06-07

Secret Service

What

The alknet-secret crate provides BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM encryption for external credentials, and the SecretProtocol irpc service. It is the only component that holds the master seed phrase.

Why

Operations like SSH key generation, API key storage, and Ethereum transaction signing all need deterministic key derivation from a single root of trust. The seed phrase is the single recovery mechanism — from it, all self-generated secrets can be derived on demand. External credentials (third-party API keys, OAuth tokens) cannot be derived and must be stored encrypted, with the encryption key itself derived from the seed.

The secret service isolates this responsibility: no other crate sees the seed, and derived keys are provided on demand through an irpc service interface.

Architecture

Security Model

State What's in memory What's on disk
Locked Nothing Encrypted database, derivation path metadata
Unlocked Master seed in RAM Same (seed is never persisted)
After use Derived keys cached in RAM Derivation paths only

The seed phrase is entered once (at node startup or via Unlock call), held only in RAM, and never written to disk. The Lock call purges the seed and all cached derived keys from memory.

SecretProtocol irpc Service

#[rpc_requests(message = SecretMessage)]
#[derive(Debug, Serialize, Deserialize)]
enum SecretProtocol {
    #[rpc(tx=oneshot::Sender<DerivedKey>)]
    #[wrap(DeriveEd25519)]
    DeriveEd25519 { path: String },

    #[rpc(tx=oneshot::Sender<DerivedKey>)]
    #[wrap(DeriveEncryptionKey)]
    DeriveEncryptionKey { path: String },

    #[rpc(tx=oneshot::Sender<DerivedKey>)]
    #[wrap(DeriveEthereumKey)]
    DeriveEthereumKey { path: String },

    #[rpc(tx=oneshot::Sender<Vec<u8>>)]
    #[wrap(DerivePassword)]
    DerivePassword { path: String, length: usize },

    #[rpc(tx=oneshot::Sender<EncryptedData>)]
    #[wrap(Encrypt)]
    Encrypt { plaintext: String, key_version: u32 },

    #[rpc(tx=oneshot::Sender<String>)]
    #[wrap(Decrypt)]
    Decrypt { encrypted: EncryptedData },

    #[rpc(tx=oneshot::Sender<()>)]
    #[wrap(Lock)]
    Lock,

    #[rpc(tx=oneshot::Sender<()>)]
    #[wrap(Unlock)]
    Unlock { passphrase: String },
}

#[derive(Debug, Serialize, Deserialize)]
struct DerivedKey {
    key_type: KeyType,
    private_key: Vec<u8>,
    public_key: Vec<u8>,
}

#[derive(Debug, Serialize, Deserialize)]
enum KeyType {
    Ed25519,
    Aes256Gcm,
    Secp256k1,
}

#[derive(Debug, Serialize, Deserialize)]
struct EncryptedData {
    key_version: u32,
    salt: String,   // Base64-encoded
    iv: String,     // Base64-encoded
    data: String,   // Base64-encoded
}

BIP39 Mnemonic and Seed Derivation

let mnemonic = Mnemonic::from_phrase(&phrase, Language::English)?;
let seed = mnemonic.to_seed(Some(&passphrase));
let master_key = ExtendedPrivKey::new_master(Network::Alknet, &seed)?;

SLIP-0010 Ed25519 HD Key Derivation

The 74' coin type is unallocated per SLIP-0044 and reserved for alknet.

Derivation Path Constants

Path Purpose Curve/Algorithm
m/74'/0'/0'/0' Primary identity keypair Ed25519 (alknet auth)
m/74'/0'/0'/{n}' Worker/device identity Ed25519
m/74'/0'/1'/0' SSH host key Ed25519
m/74'/1'/0'/{hash}' Site-specific password Deterministic
m/74'/2'/0'/0' Encryption key for external credentials AES-256-GCM
m/44'/60'/0'/0/0 Ethereum signing key secp256k1

AES-256-GCM Encryption for External Credentials

External credentials (API keys, OAuth tokens) that cannot be derived are encrypted using a key derived from the seed at path m/74'/2'/0'/0'. The EncryptedData type stores the key version, salt, IV, and ciphertext. This format is compatible with the existing @alkdev/storage EncryptedDataSchema.

  1. The secret service derives an AES-256-GCM key via path m/74'/2'/0'/0'
  2. External credentials are encrypted with this key
  3. The encrypted data is stored as a SecretNode in the metagraph
  4. Only the derivation path and key version are stored in plain attributes
  5. The seed phrase (or derived encryption key) is held only by the secret service — never in the database

Deployment Topologies

Minimal (single node, CLI): Secret service runs in the same process. Seed phrase entered at startup. All keys derived locally. No irpc overhead.

Production (head node): Secret service runs on a dedicated node or as a local irpc service. Workers request derived keys via irpc over QUIC. The seed never leaves the secret service node.

Constraints

  • The seed phrase is never persisted to disk. It is entered at startup or via Unlock and held only in RAM.
  • Lock purges the seed and all cached derived keys from memory.
  • alknet-secret does not depend on alknet-core or alknet-storage. It is fully independent.
  • The EncryptedData wire format (key_version, salt, iv, data) is shared with alknet-storage for compatibility, but this is type-level compatibility — not a crate dependency.
  • Per ADR-032, the secret service's Honker streams (key derivation notifications) stay within the service boundary. External consumers use irpc calls or call protocol operations that project to integration events.
  • The irpc service defines the wire format for in-cluster communication (postcard serialization). For call protocol exposure (e.g., /head/secrets/derive), the service is wrapped in an operation that serializes to JSON.

Open Questions

  • OQ-SVC-01: Should the secret service support multiple seed phrases (one per tenant)? See open-questions.md.

  • OQ-SVC-02: Should service protocols use postcard (binary) or JSON for remote calls? See open-questions.md.

  • OQ-SVC-03: How does the secret service integrate with the existing EncryptedDataSchema from @alkdev/storage? See open-questions.md.

  • OQ-SVC-04: Should workers cache derived keys locally? See open-questions.md.

Design Decisions

ADR Decision Summary
027 Crate decomposition alknet-secret is independent of core and storage
032 Event boundary Secret service domain events stay internal

References