docs: write Phase 0 architecture foundation — ADRs 026-034, spec docs, and task updates
Phase 0a — ADRs (9 new): - ADR-026: Transport/interface separation (three-layer model) - ADR-027: Crate decomposition (core, secret, storage, flowgraph, napi, CLI) - ADR-028: Auth as irpc service (AuthProtocol behind feature flag) - ADR-029: Identity as core type (Identity + IdentityProvider in alknet-core) - ADR-030: Static/dynamic config split (ArcSwap, ConfigReloadHandle) - ADR-031: Forwarding policy (rule-based allow/deny, TransportKind-aware) - ADR-032: Event boundary discipline (domain, irpc, call protocol boundaries) - ADR-033: OperationEnv universal composition (three dispatch paths) - ADR-034: Head/worker terminology (replace hub/spoke) Phase 0b — New spec documents (7): - identity.md, services.md, interface.md, configuration.md, storage.md, flowgraph.md, secret-service.md Updated existing docs: - auth.md: reference identity.md for canonical definitions, add AuthProtocol - open-questions.md: resolve OQ-12, OQ-16, OQ-18, OQ-22, OQ-23-25 - README.md: add all new docs, ADRs 026-034 Marked 19 architecture tasks as completed.
This commit is contained in:
197
docs/architecture/secret-service.md
Normal file
197
docs/architecture/secret-service.md
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 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
|
||||
|
||||
```rust
|
||||
#[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
|
||||
|
||||
```rust
|
||||
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](open-questions.md).
|
||||
|
||||
- **OQ-SVC-02**: Should service protocols use postcard (binary) or JSON for
|
||||
remote calls? Postcard for irpc (Rust-to-Rust), JSON for call protocol
|
||||
(cross-language). See [open-questions.md](open-questions.md).
|
||||
|
||||
- **OQ-SVC-03**: How does the secret service integrate with the existing
|
||||
`EncryptedDataSchema` from `@alkdev/storage`? The Rust implementation replaces
|
||||
PBKDF2 password-based encryption with derived AES-256-GCM keys. The
|
||||
`EncryptedData` format is a superset.
|
||||
|
||||
- **OQ-SVC-04**: Should workers cache derived keys locally? Yes, with a TTL
|
||||
(default: 1 hour). The head can revoke by invalidating the session.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| ADR | Decision | Summary |
|
||||
|-----|----------|---------|
|
||||
| [027](decisions/027-crate-decomposition.md) | Crate decomposition | alknet-secret is independent of core and storage |
|
||||
| [032](decisions/032-event-boundary-discipline.md) | Event boundary | Secret service domain events stay internal |
|
||||
|
||||
## References
|
||||
|
||||
- [research/services.md](../research/services.md) — SecretProtocol definition, DerivedKey, KeyType
|
||||
- [research/storage.md](../research/storage.md) — Secrets section, derivation paths, EncryptedData
|
||||
- [research/integration-plan.md](../research/integration-plan.md) — Phase 2.1
|
||||
- SLIP-0010 — https://github.com/satoshilabs/slips/blob/master/slip-0010.md
|
||||
- BIP39 — https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
|
||||
Reference in New Issue
Block a user