docs(architecture): resolve review #003 — type/API surface completeness
Review #003 found 11 critical, 14 warning, and 6 suggestion findings after reviews #001 (governance/security) and #002 (cross-document consistency/two-way-door audit) were resolved. The theme: types and APIs that were *referenced* but never *defined*, and stale ADR sketches that didn't match the now-updated spec docs. Critical fixes (11): - C1: DerivedKey #[derive(Deserialize)] contradicted the custom Deserialize that rejects "[REDACTED]" — dropped the derive, added explicit manual Serialize/Deserialize impls (protocol.md). - C2: encrypt prose said "derived at PATHS::ENCRYPTION" but the signature takes key_version — updated to encryption_path_for_version (service.md). - C3: derive_encryption_key returned DerivedKey, derive_encryption_key _for_version returned EncryptionKey (same cache) — unified on DerivedKey, defined CachedKey (service.md). - C4: tokio vs std::sync::RwLock contradiction — specified std::sync::RwLock, dropped tokio from vault deps (ADR-018, ADR-025, service.md). - C5: Missing drift rows in vault README — added #9 (key_version ignored) and #10 (rotate not implemented). - C6: ADR-022 build_root_context and invoke() sketches omitted abort_policy (9 fields vs 10) — added the field to both sketches. - C7: Capabilities type referenced 20+ times, never defined — added struct definition to core-types.md with Clone+Send+Sync, Zeroize, sealed builder API, immutability guard. - C8: SessionOverlaySource on CallAdapter but never defined, crate violation (alknet-call can't depend on alknet-agent) — defined the trait in alknet-call (call-protocol.md), matching the IdentityProvider pattern. - C9: CompositeOperationEnv dispatch fall-through was "a two-way door" — added contains() to OperationEnv trait, made the composite probe before dispatching, eliminating the sentinel ambiguity. - C10: No API for Layer 2 (connection overlay) registration, CallConnection undefined — defined CallConnection struct + register_imported() API (call-protocol.md). - C11: with_local signature diverged between two examples (4 args vs 5) — added capabilities as the 5th arg, made both examples consistent. Warning fixes (14): - W1: invoke_with_policy restructured as required method, invoke gets a default impl delegating to it — eliminates duplication across impls. - W2: CachedKey defined (service.md). - W3: EncryptionKey constructor/glue specified, added to re-export list. - W4: Secp256k1ExtendedPrivKey defined, derive_ethereum_key glue shown. - W5: encryption_path_for_version rejects version < 2 (v1 is TS PBKDF2). - W6: Wire payload schemas for all event types + ResponseEnvelope → EventEnvelope conversion table (call-protocol.md). - W7: Timeout section — deadline on OperationContext, composed calls inherit parent's deadline, CallAdapter::with_timeout(). - W8: Request ID generation spec — UUID v4 for composed calls, wire ID vs internal ID relationship for abort cascade. - W9: unlock_new already-unlocked behavior specified (returns AlreadyUnlocked). - W10: KeyType Serialize/Deserialize justification corrected (stale irpc reference removed). - W11: OperationProvenance and CompositionAuthority defined inline in operation-registry.md (were only in ADR-022). - W12: encrypt/decrypt free functions marked pub(crate), relationship to VaultServiceHandle methods stated. - W13: rotate signature removed from encryption.md (it's a VaultServiceHandle method, not a free function). - W14: CallAdapter::new() + with_session_source() + with_timeout() constructors shown. Suggestion fixes (6): Seed: Clone note, VaultServiceInner invariant, ExtendedPrivKey accessor signatures, CURRENT_KEY_VERSION location, ADR-018 stale actor text, derivation helpers re-export note.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-22-25
|
||||
last_updated: 2026-06-23
|
||||
---
|
||||
|
||||
# Protocol
|
||||
@@ -26,7 +26,7 @@ The result of key derivation. Holds the key type, private key, and public
|
||||
key.
|
||||
|
||||
```rust
|
||||
#[derive(Zeroize, Deserialize)]
|
||||
#[derive(Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct DerivedKey {
|
||||
#[zeroize(skip)]
|
||||
@@ -38,6 +38,12 @@ pub struct DerivedKey {
|
||||
}
|
||||
```
|
||||
|
||||
`DerivedKey` does **not** derive `Deserialize` via `#[derive]`. It has a **custom
|
||||
`Deserialize` impl** that rejects redacted payloads — see
|
||||
[Serialization Redaction](#serialization-redaction) below. (A derived
|
||||
`Deserialize` would generate a default impl that conflicts with the manual one,
|
||||
and would not produce the explicit redaction-rejection error the spec requires.)
|
||||
|
||||
The `#[zeroize(skip)]` attributes on `key_type` and `public_key` mean only
|
||||
the `private_key` is zeroized when the `DerivedKey` is dropped. The public
|
||||
key and key type are not secret material — zeroizing them is unnecessary
|
||||
@@ -65,10 +71,13 @@ private key, regardless of format:
|
||||
`"[REDACTED]"`. This is defense-in-depth — if a `DerivedKey` accidentally
|
||||
ends up in a log, a JSON config, or debug output, the private key is not
|
||||
exposed.
|
||||
- **Deserialization**: rejects `private_key == "[REDACTED]"` with an error.
|
||||
A JSON-deserialized `DerivedKey` with a redacted private key is invalid
|
||||
and produces a deserialization error, not a corrupted key. This resolves
|
||||
review #002 W8 (silent corruption on JSON-deserialized `DerivedKey`).
|
||||
- **Deserialization**: a custom `Deserialize` impl rejects
|
||||
`private_key == "[REDACTED]"` with a deserialization error (not a corrupted
|
||||
key). This resolves review #002 W8 (silent corruption on JSON-deserialized
|
||||
`DerivedKey`). The custom impl is required because `#[derive(Deserialize)]`
|
||||
would generate a default impl that conflicts and would only fail incidentally
|
||||
(serde type mismatch: string vs sequence), not with the explicit
|
||||
redaction-rejection error the spec requires.
|
||||
- **No binary-format preservation path.** ADR-025 dropped the postcard/remote
|
||||
dispatch path that previously preserved private key bytes in binary
|
||||
formats. `DerivedKey` is always used in-process (ADR-014: never appears
|
||||
@@ -76,6 +85,49 @@ private key, regardless of format:
|
||||
`DerivedKey` over the wire, it defines its own serialization for that
|
||||
context — the vault's `DerivedKey` stays redact-always.
|
||||
|
||||
```rust
|
||||
// Custom Serialize — always redacts private_key
|
||||
impl serde::Serialize for DerivedKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: serde::Serializer {
|
||||
use serde::SerializeStruct;
|
||||
let mut s = serializer.serialize_struct("DerivedKey", 3)?;
|
||||
s.serialize_field("key_type", &self.key_type)?;
|
||||
s.serialize_field("private_key", "[REDACTED]")?; // never the real bytes
|
||||
s.serialize_field("public_key", &self.public_key)?;
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Deserialize — rejects "[REDACTED]" with an error
|
||||
impl<'de> serde::Deserialize<'de> for DerivedKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where D: serde::Deserializer<'de> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct DerivedKeyHelper {
|
||||
key_type: KeyType,
|
||||
private_key: Vec<u8>,
|
||||
public_key: Vec<u8>,
|
||||
}
|
||||
let helper = DerivedKeyHelper::deserialize(deserializer)?;
|
||||
// Reject redacted payloads — a JSON-deserialized DerivedKey with a
|
||||
// redacted private key is invalid, not a corrupted key.
|
||||
if helper.private_key == b"[REDACTED]" {
|
||||
return Err(serde::de::Error::custom(
|
||||
"DerivedKey.private_key is \"[REDACTED]\" — redacted payloads \
|
||||
cannot be deserialized. JSON round-tripping a DerivedKey is \
|
||||
not supported (the private key is gone)."
|
||||
));
|
||||
}
|
||||
Ok(DerivedKey {
|
||||
key_type: helper.key_type,
|
||||
private_key: helper.private_key,
|
||||
public_key: helper.public_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The redaction is **not the primary control** for keeping private keys off
|
||||
the wire. The primary control is architectural: `DerivedKey` never appears
|
||||
in call protocol payloads (ADR-014). The redaction is a safety net for
|
||||
@@ -112,8 +164,10 @@ pub enum KeyType {
|
||||
```
|
||||
|
||||
Tags `DerivedKey` and `CachedKey` so consumers know what they received.
|
||||
`KeyType` is `Serialize`/`Deserialize` (it's part of the irpc protocol) and
|
||||
`Clone` (it's not secret material — it's a tag).
|
||||
`KeyType` is `Serialize`/`Deserialize` (retained for `EncryptedData` interop
|
||||
and future use — ADR-025 removed the irpc dispatch path that previously
|
||||
justified these derives, but the type remains serializable for structured
|
||||
storage scenarios) and `Clone` (it's not secret material — it's a tag).
|
||||
|
||||
## Wire Format
|
||||
|
||||
|
||||
Reference in New Issue
Block a user