- Add irpc (0.16) and irpc-derive (0.16) as workspace dependencies - Add irpc, irpc-derive, and secp256k1 (optional) to alknet-secret Cargo.toml - Clarify encryption-salt-kdf task: Option B (document salt as reserved) is the chosen path per spec update, removing Option A acceptance criteria - Update irpc-secret-protocol-integration task with concrete irpc crate details: real crate on crates.io v0.16, #[rpc_requests] macro, workspace config, AuthProtocol pattern reference, DerivedKey serialization considerations - Fix secp256k1-ethereum-derivation task: correct crate name is secp256k1 (not libsecp256k1), add version pin 0.29
9.7 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||
|---|---|---|---|---|---|---|---|---|---|
| irpc-secret-protocol-integration | Wire SecretProtocol to irpc with local SecretServiceHandle and remote dispatch | pending |
|
moderate | medium | component | implementation |
Description
The SecretProtocol enum in protocol.rs currently has a placeholder SecretMessage = SecretProtocol type alias. The spec (after update) defines two dispatch paths:
- Local dispatch (in-process):
SecretServiceHandle— async methods that directly call intoSecretServiceInner. No serialization overhead. - Remote dispatch (in-cluster):
SecretProtocolirpc client — sendsSecretMessagevia mpsc (local node) or QUIC stream (remote worker). The service runs on a head node; workers request derived keys via irpc.
Per ADR-027, irpc is always-on in alknet-secret (not feature-gated). Per ADR-033, irpc is one dispatch backend for OperationEnv.
irpc Crate Details
The irpc crate (version 0.16.0 on crates.io) provides the #[rpc_requests] derive macro that generates message enums with channel types. This is the same pattern used by n0/iroh projects.
Key irpc concepts:
#[rpc_requests(message = SecretMessage)]on theSecretProtocolenum generates aSecretMessageenum where each variant wraps the inner type inWithChannels<Inner, SecretProtocol>- Each variant annotated with
#[rpc(tx=oneshot::Sender<T>)]gets aoneshot::Sender<T>channel for responses Client<SecretProtocol>is the client type that can send messages locally (via mpsc) or remotely (via noq/QUIC)- The
rpcfeature in irpc is enabled by default and includes the remote transport (postcard + noq) - The
derivefeature in irpc enables the#[rpc_requests]macro
Current pattern in alknet-core for reference:
AuthProtocolin alknet-core (crates/alknet-core/src/auth/auth_protocol.rs) is currently a plain enum with synchronous methods onAuthServiceImpl— it does NOT use#[rpc_requests]yet. The alknet-core irpc feature flag exists but is empty. This is because alknet-core's irpc integration hasn't been implemented yet.- alknet-secret should use the actual
irpccrate with#[rpc_requests]since it's the first crate to do the irpc integration properly.
Workspace configuration:
irpc = "0.16"needs to be added to the workspaceCargo.toml[workspace.dependencies]sectionalknet-secret/Cargo.tomlneedsirpc = { workspace = true }andirpc-derive = { workspace = true }(the derive macro is in a separate crate)
irpc dependency requirements:
irpcwith default features brings innoq(QUIC transport),postcard(serialization), andtokio. These are acceptable for alknet-secret.- The
derivefeature is needed for#[rpc_requests].
What needs to happen
-
Add irpc as a workspace dependency: Add
irpc = "0.16"andirpc-derive = "0.16"to the workspaceCargo.toml[workspace.dependencies]section. Addirpc = { workspace = true }andirpc-derive = { workspace = true }toalknet-secret/Cargo.toml. -
Replace
SecretMessagetype alias with irpc-generated type: Apply#[rpc_requests(message = SecretMessage)]toSecretProtocolwith appropriate#[rpc(tx=oneshot::Sender<T>)]attributes on each variant. This generates:SecretMessageenum withWithChannelswrappersChannels<SecretProtocol>impl for each variant typeFrom<VariantType> for SecretProtocolimplsServiceandRemoteServiceimpls forSecretProtocol
-
Update SecretProtocol enum for irpc: The current enum has plain variants like
DeriveEd25519 { path: String }. With irpc's#[wrap]attribute, each variant gets a wrapper struct:#[rpc_requests(message = SecretMessage)] #[derive(Debug, Serialize, Deserialize)] pub 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 }, } -
Create SecretServiceActor: Wrap
SecretServiceHandlein an actor that processesSecretMessagevariants and sends responses through the oneshot channels. The actor runs as atokio::task:pub struct SecretServiceActor { handle: SecretServiceHandle, } impl SecretServiceActor { pub async fn run(mut self, mut rx: tokio::sync::mpsc::Receiver<SecretMessage>) { ... } pub fn handle(&self) -> &SecretServiceHandle { &self.handle } } -
Keep SecretServiceHandle as primary local API: The
RwLock<SecretServiceInner>pattern stays for direct in-process use. The actor wraps it for irpc dispatch. -
Update public API: Re-export
SecretMessage,SecretServiceActor, andClient<SecretProtocol>fromlib.rs. -
Handle DerivedKey serialization for irpc: Since
DerivedKeywill become non-Clone (perderivedkey-zeroize-securitytask) and needs custom serialization that redactsprivate_key, ensure the irpc wire format works correctly. The#[wrap]structs andSecretMessageneed to serialize/deserializeDerivedKey— since irpc usespostcardfor remote transport, theSerialize/Deserializeimpls must handle the redactedprivate_keyfield appropriately. For local (mpsc) transport,DerivedKeyis sent through oneshot channels without serialization, so the redacted serialization only matters for remote transport.
Acceptance Criteria
irpcandirpc-deriveadded as workspace dependencies in rootCargo.tomlirpcandirpc-deriveadded toalknet-secret/Cargo.tomlas workspace dependenciesSecretProtocolenum annotated with#[rpc_requests(message = SecretMessage)]and#[rpc(tx=...)]attributesSecretMessageis no longer a type alias — it's the irpc-generated message typeSecretServiceActorstruct that wrapsSecretServiceHandleand processesSecretMessagevariantsSecretServiceActor::run()method that spawns a message loop as atokio::taskSecretServiceActor::spawn()method that returns aClient<SecretProtocol>for sending messages- Each
SecretMessagevariant dispatches to the correspondingSecretServiceHandlemethod and sends response through oneshot channel SecretServiceHandleremains the primary local API (RwLock-based, unchanged for direct use)- Unit test:
SecretServiceActorprocessesSecretMessage::Unlockand responds successfully - Unit test:
SecretMessage::DeriveEd25519dispatched through actor returnsDerivedKey - Unit test:
SecretMessage::Lockclears state and subsequent derive calls fail protocol.rsupdated:SecretMessageis the irpc-generated message type, not a type aliaslib.rsre-exports updated to includeSecretServiceActorandClient<SecretProtocol>cargo test -p alknet-secretpasses with all existing testscargo clippy -p alknet-secret -- -D warningspassescargo fmt -p alknet-secret -- --checkpasses
References
- docs/architecture/secret-service.md — irpc service section (after spec update)
- docs/architecture/decisions/027-crate-decomposition.md — ADR-027 (irpc always-on in alknet-secret)
- docs/architecture/decisions/033-operationenv-irpc-call-protocol.md — ADR-033 (irpc as dispatch backend)
- crates/alknet-secret/src/protocol.rs — Current SecretProtocol with placeholder SecretMessage
- crates/alknet-secret/src/service.rs — SecretServiceHandle and SecretService
- irpc crate (crates.io v0.16) —
#[rpc_requests]derive macro,Client<S>type,WithChannels,Channelstrait - crates/alknet-core/src/auth/auth_protocol.rs — AuthProtocol pattern (reference, but note: NOT using irpc yet)
Notes
The irpc crate is on crates.io at version 0.16.0. Use
irpc = "0.16"andirpc-derive = "0.16"as workspace dependencies. Do NOT use a local path dependency.
The
#[rpc_requests]macro generates: (1) aSecretMessageenum withWithChannelswrappers for each variant, (2)Channels<SecretProtocol>impls, (3)Fromimpls, (4)ServiceandRemoteServiceimpls. See the irpc crate docs and examples for the exact generated code structure.
The
SecretServiceHandlewithRwLockshould remain as the primary local API. It's simpler, faster, and works well for single-process use. TheSecretServiceActorwraps it for irpc dispatch. This two-API pattern matches the spec's "minimal deployment (local handle) vs production deployment (irpc service)" distinction.
Since
DerivedKeyis becoming non-Clone with redacted serialization (perderivedkey-zeroize-security), the irpc integration needs to handle this. For local (mpsc) transport,DerivedKeymoves through oneshot channels without serialization — no issue. For remote (postcard) transport,DerivedKeyneeds proper Serialize/Deserialize. The custom serialization should serializeprivate_keyas bytes (not redacted) for postcard since it's a binary format used for in-cluster Rust-to-Rust communication — the redaction is for JSON/debug output only.
Summary
To be filled on completion