Apply #[rpc_requests(message = SecretMessage)] to SecretProtocol enum with
#[rpc(tx=oneshot::Sender<Result<T, SecretServiceError>>)] and #[wrap] attributes
on each variant. Add SecretServiceActor that wraps SecretServiceHandle and
processes SecretMessage variants via mpsc channel. Update DerivedKey
serialization to use is_human_readable() so postcard preserves private_key
bytes while JSON redacts them. Add Serialize/Deserialize to SecretServiceError
for irpc wire format compatibility. Add tokio dependency for actor runtime.
Implement KeyCache in cache.rs with CachedKey (Zeroize-protected),
CacheConfig (TTL + max_entries), and lazy eviction. Wire cache into
SecretServiceInner: derive_* methods check cache before re-deriving,
Lock clears and zeroizes all cache entries, encrypt/decrypt use
cached encryption key. Per ADR-038 and OQ-SVC-04.
Implements deterministic password derivation using SLIP-0010 at the
given path. derive_password returns raw truncated key bytes;
derive_password_string returns Base64url-encoded (no padding) string.
Both require unlocked state (ServiceLocked if locked). Includes unit
tests for determinism, different paths, length truncation, locked
error, and Base64url encoding.
Per ADR-038, DerivedKey.private_key now derives Zeroize with #[zeroize(drop)]
ensuring sensitive key material is zeroized before deallocation. DerivedKey
is now move-only (no Clone), and JSON/debug output redacts private_key as
"[REDACTED]". Deserialization still works for postcard/irpc wire format.
Also fixes clippy needless_borrows_for_generic_args in encryption.rs and
applies cargo fmt to existing code.
Add module-level documentation explaining that the salt field is reserved
for Phase B KDF-based key rotation. Add doc comment on the salt field
clarifying it is not used in v1 key derivation. Add TODO(Phase B) comment
on salt generation in encrypt().
- 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
Create the alknet-secret crate with BIP39 mnemonic generation, SLIP-0010
Ed25519 HD key derivation, AES-256-GCM encryption, and SecretProtocol
irpc service definition. This is Phase 3.1 from the integration plan.
Architecture changes:
- Promote secret-service.md to reviewed status with full spec format
(crate structure, public API, security model, phase progression,
ADR/OQ cross-references, wire format compatibility section)
- Add ADR-038 (seed lifecycle and memory security): zeroize for v1,
mlock deferred to Phase B
- Add OQ-SEC-01 (mlock/VirtualLock for seed RAM) to open-questions.md
- Update README.md with ADR-038 and secret-service status
Crate structure:
- src/mnemonic.rs: BIP39 phrase generation, validation, seed derivation
- src/derivation.rs: SLIP-0010 HD key derivation, path constants (74')
- src/encryption.rs: AES-256-GCM encrypt/decrypt, EncryptedData type
- src/protocol.rs: SecretProtocol irpc enum, DerivedKey, KeyType
- src/service.rs: SecretServiceHandle with Unlock/Lock lifecycle
- 40 passing tests (unit + integration + doc)
Add http feature flag with axum, hyper, hyper-util, tower, and http-body-util
dependencies. Create http module with auth middleware (extracts Bearer token,
calls IdentityProvider::resolve_from_token, attaches Identity to extensions)
and router scaffold (default 404 fallback, no operational routes yet). Replace
send_fake_nginx_404 with axum router handoff when http feature is enabled;
fake 404 behavior preserved when http is disabled. Wire HttpInterface with
build_router() method and pass IdentityProvider through Server to handle_connection.
Implement the SSH session to call protocol bridge:
- Add FrameFramedReader/FrameFramedWriter for async I/O of length-prefixed EventEnvelope frames
- SshSession::recv() reads InterfaceEvent frames from the alknet-control:0 channel via mpsc
- SshSession::send() writes EventEnvelope frames to the alknet-control:0 channel via mpsc
- Add ControlChannelBridge implementing ControlChannelHandler for routing channel data
- SshHandler::channel_open_direct_tcpip routes alknet-control:0 to the bridge task
- Session Identity attached to every InterfaceEvent produced by recv()
- ControlChannelRouter gains take_handler() for non-control alknet-* channel routing
Implement RawFramingSession with tokio::io::split for read/write halves,
BufReader/BufWriter for buffered I/O, and decode_with_remainder for
partial frame reassembly. Add first-frame authentication via
IdentityProvider::resolve_from_token(). Add RawFramingConfig.auth
field for IdentityProvider reference. Update SshInterface test for
new RawFramingConfig shape.
Define the outbound authentication abstraction in alknet_core::credentials:
- CredentialProvider trait with get_credentials and refresh_credentials
- CredentialSet enum with ApiKey, Basic, Bearer, S3AccessKey, OidcToken, Custom variants
- ConfigCredentialProvider reads credentials from DynamicConfig.credentials
- SecretStoreCredentialProvider stub returns None for all lookups (Phase 3)
- Wire CredentialProvider into OperationEnv via credentials() method
- Add credentials HashMap field to DynamicConfig
Per ADR-035: split Interface trait into StreamInterface (stream-based, SSH/RawFraming)
and MessageInterface (request/response, HTTP/DNS). Remove TransportKind::Dns (DNS is
a MessageInterface). Change WebTransport { host } to { server_name: Option<String> }.
Restructure ListenerConfig from flat struct to enum with Stream/Http/Dns variants.
Add doc comments and TODO markers to SshSession::recv() and send()
explicitly marking them as Phase 1 stubs. Notes that call protocol
event bridging from SSH channels is planned for Phase 2/3.
New Phase 1 modules should follow the existing pattern of referencing
ADR numbers in module-level doc comments for discoverability, matching
the style in transport/mod.rs.
ForwardingAction, TargetPattern, ForwardingRule, OperationType,
InterfaceConfig, InterfaceKind, DynamicConfig, and CallError are all
likely to gain variants/fields in future phases. Marking them
#[non_exhaustive] now prevents downstream breakage when new
variants/fields are added. Added constructor methods for types that
are constructed from other crates.
parse_proxy_config was using expect()/unwrap()/panic!() which would
crash the process on malformed proxy config strings instead of
returning a descriptive error. Now returns ConfigError::ProxyConfigInvalid
with the specific issue (bad scheme, bad address). Added tests for
invalid scheme, invalid address, and end-to-end from_serve_options.
NapiServerHandler was bypassing IdentityProvider, calling
config.auth.authenticate_publickey() directly, which meant no Identity
was stored on the session and per-identity forwarding rules could not
match. It also skipped ForwardingPolicy::check() entirely, defeating
forwarding access control for NAPI-served tunnels. Both are now
consistent with ServerHandler and SshHandler behavior.
- SshInterface implements Interface trait with accept() method
- SshSession implements InterfaceSession trait (stub for call protocol events)
- RawFramingInterface is type-only stub (Phase 4+ for DNS, WebTransport)
- TransportKind consolidated into transport module with Display, PartialEq, Eq
- ListenerConfig gains interface_kind field for (Transport, Interface) pairs
- SshInterface wraps existing russh handler logic (SshHandler)
- Auth delegation through IdentityProvider (not embedded in SshInterface)
- Channel routing through session to Layer 3 (forwarding policy)
- Server accept loop uses (Transport, Interface) pairs
Per ADR-026: SSH is Layer 2, not Layer 1. This is the highest-risk Phase 1
task, implementing the Interface trait to separate transport from interface.
- Add reloadAuth(), reloadForwarding(), reloadAll() methods to AlknetServer
- Add NAPI type definitions: AuthConfigNapi, ForwardingPolicyConfig, ForwardingRuleConfig
- Refactor NapiServerHandler to use ArcSwap<DynamicConfig> for atomic config swaps
- Add ConfigReloadHandle::dynamic_arc() accessor for sharing ArcSwap between NAPI and accept loop
- Add ipnetwork dependency to alknet-napi for TargetPattern CIDR parsing
- Add builder functions for AuthPolicy and ForwardingPolicy from NAPI config types
- All swaps are atomic via ArcSwap per ADR-030
Add Layer 2 interface abstraction per ADR-026:
- Interface trait with accept() and associated Session type
- InterfaceConfig enum with Ssh and RawFraming variants
- SshInterfaceConfig with auth, forwarding, host_key fields
- RawFramingConfig (minimal, no SSH-specific config)
- InterfaceSession trait with recv()/send() producing InterfaceEvent frames
- InterfaceEvent wraps EventEnvelope with optional Identity
- Resolves OQ-IF-01: every session produces EventEnvelope frames
via InterfaceSession, making Layer 3 interface-agnostic
- Valid (Transport, Interface) pair enumeration with
TransportKindBase and is_valid_pair validation function
- Module re-exported from lib.rs
- Change ServerHandler to hold Arc<dyn IdentityProvider> instead of Box<dyn IdentityProvider>
- Refactor Server::new() to use StaticConfig::from_serve_options() producing (StaticConfig, DynamicConfig)
- Remove duplicate parse_proxy_config from serve.rs (now in static_config.rs)
- Add with_identity_provider() accepting Arc<dyn IdentityProvider>
- Add integration tests for DynamicConfig reload and ForwardingPolicy deny
- Add test for custom IdentityProvider injection via with_identity_provider
- Move parse_proxy_config tests to static_config.rs module
Add local dispatch for OperationEnv with invoke() method, EventEnvelope
wire format struct, 4-byte BE length-prefixed frame encoding/decoding,
PendingRequestMap for call/subscribe correlation, call protocol event type
constants, and default /services/list and /services/schema operations.
Add ForwardingPolicy, ForwardingAction, ForwardingRule, and TargetPattern
types in config/forwarding.rs. Implement policy evaluation with first-match
wins semantics, principal and transport matching, CIDR and glob patterns.
Modify ServerHandler to check ForwardingPolicy before proxying in
channel_open_direct_tcpip. Reserved alknet-* destinations bypass policy.
Preserve existing behavior with default allow_all() policy.
Add AuthProtocol enum (VerifyPubkey, VerifyToken, ReloadKeys, CheckAccess),
AuthResult enum (Ok(Identity), Denied(String)), and AuthServiceImpl
wrapping ConfigIdentityProvider via ArcSwap<DynamicConfig>. All gated
behind the irpc feature flag per ADR-028.
- Add ListenerConfig struct with transport_kind, listen_addr, per-transport config
- Add Dns and WebTransport variants to TransportKind (tags only, no behavior)
- Add .listeners() builder method to ServeOptions for multi-listener config
- Keep .transport_mode() backwards compatible (creates single-element listeners vec)
- Update Server::run() to use listeners from Server struct (first listener)
- Add Server::listeners() accessor for multi-transport listener configs
- Update StaticConfig to support listeners field, converted from ServeOptions
- All listeners share Arc<ArcSwap<DynamicConfig>>, ConnectionRateLimiter, and IdentityProvider
- Graceful shutdown terminates accept loop via existing shutdown signal
- TOML [[listeners]] array-of-tables syntax supported via ListenerConfig in StaticConfig
- Add comprehensive tests for ListenerConfig, multi-listener ServeOptions, Server creation
Add Identity struct with id/scopes/resources fields and IdentityProvider
trait with resolve_from_fingerprint/resolve_from_token methods. Implement
ConfigIdentityProvider reading from ArcSwap<DynamicConfig.auth> for
fingerprint-based key lookups. Delegate ServerHandler::auth_publickey()
through IdentityProvider instead of direct AuthPolicy access. Store
authenticated Identity in the handler for use by ForwardingPolicy.
ConfigServiceImpl wraps ArcSwap<DynamicConfig> providing forwarding_policy(),
rate_limits(), and reload() methods for direct use (always available).
ConfigProtocol enum (GetForwardingPolicy, GetRateLimits, ReloadForwarding,
ReloadRateLimits) is gated behind the irpc feature flag per ADR-030.
Split alknet-core configuration into StaticConfig (immutable after startup)
and DynamicConfig (hot-reloadable at runtime via ArcSwap).
- Add StaticConfig struct in config/static_config.rs with all fields per ADR-030
- Add DynamicConfig struct with AuthPolicy, ForwardingPolicy, RateLimitConfig
- Add ForwardingPolicy with allow_all()/deny_all() defaults (ADR-031)
- Add ConfigReloadHandle with reload() method for runtime config updates
- Replace Arc<ServerAuthConfig> with Arc<ArcSwap<DynamicConfig>> in ServerHandler
- Add config_reload_handle() to Server for obtaining reload handles
- Add AuthPolicy with authenticate_publickey/authenticate_certificate methods
- All existing tests pass with the new config structure
- Default DynamicConfig produces identical behavior to current code
Enable running wraith alongside iroh-blobs, iroh-gossip, and iroh-docs
on the same QUIC endpoint (one connection per peer, multiplexed by ALPN).
- IrohTransport::from_endpoint(node_id, endpoint) for client-side shared endpoint
- IrohAcceptor::from_endpoint(endpoint) for server-side shared endpoint
- Export ALPN constant as IROH_ALPN for Router registration
- Add owned() method to track whether the endpoint was created internally
- Existing new()/bind() constructors unchanged (backwards compatible)
- Add tests for from_endpoint constructors and shared endpoint connectivity
- Restrict auth methods to PUBLICKEY only (no none, password, hostbased,
or keyboard-interactive advertised during negotiation)
- Log all denied channel types (session, x11, forwarded-tcpip) and
dangerous request types (exec, shell, subsystem, pty, env, x11, agent)
- Explicitly reject all dangerous channel request handlers (exec, shell,
subsystem, pty, env, x11, agent forwarding) with channel_failure
responses instead of russh's default silent Ok(()) which leaves clients
hanging and is a footgun if session channels are ever allowed
- Explicitly reject tcpip_forward, streamlocal_forward with logged warnings
- Log signal requests at debug level (harmless, no response needed)
- Override handlers in both core ServerHandler and NapiServerHandler
- Add tracing dependency to wraith-napi for security event logging
- Set preferred algorithms explicitly (russh::Preferred::DEFAULT which
uses only modern KEX/cipher/MAC algorithms)