5.6 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| vault/derivedkey-serialization | Implement always-redact DerivedKey serialization and reject redacted payloads on deserialize | completed |
|
narrow | medium | component | implementation |
Description
Fix drift item #5: DerivedKey currently has dual serialization behavior — JSON
redacts the private key, but postcard (the binary format used by irpc) preserves
the raw bytes. ADR-025 dropped the postcard/remote path, so DerivedKey should
always redact on serialize and reject "[REDACTED]" on deserialize with an
explicit error.
Current state
protocol.rs has DerivedKey with #[derive(Serialize, Deserialize)] (or
similar) that produces JSON redaction for JSON but preserves bytes in postcard.
The postcard tests in the test suite verify the binary round-trip.
Target state
Per docs/architecture/crates/vault/protocol.md → Serialization Redaction:
DerivedKey must not derive Deserialize via #[derive]. It needs custom
Serialize and Deserialize impls:
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]")?;
s.serialize_field("public_key", &self.public_key)?;
s.end()
}
}
Custom Deserialize — rejects "[REDACTED]" with an explicit 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)?;
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,
})
}
}
Debug impl — also redacts:
impl fmt::Debug for DerivedKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DerivedKey")
.field("key_type", &self.key_type)
.field("private_key", &"[REDACTED]")
.field("public_key", &self.public_key)
.finish()
}
}
Remove postcard tests
The postcard round-trip tests (which verified binary format preserved private
key bytes) are removed — ADR-025 dropped that path. The postcard
dev-dependency was removed in the irpc removal task (drift #4).
Why custom impls instead of derives
A derived Deserialize would generate a default impl that conflicts with the
manual one, and would only fail incidentally (serde type mismatch: string vs
sequence), not with the explicit redaction-rejection error the spec requires.
The custom impl is required for the explicit error message.
Scope
This task touches protocol.rs (the DerivedKey type, its serde impls, Debug
impl) and test files (remove postcard tests, add redaction tests). It depends on
the irpc removal task (drift #4) because both modify protocol.rs.
Acceptance Criteria
DerivedKeydoes not deriveSerializeorDeserializevia#[derive]- Custom
Serializeimpl always redactsprivate_keyas"[REDACTED]" - Custom
Deserializeimpl rejectsprivate_key == b"[REDACTED]"with explicit error - Custom
Debugimpl redactsprivate_keyas"[REDACTED]" - Postcard round-trip tests removed
- Unit test: JSON serialize produces
"[REDACTED]"forprivate_key - Unit test: JSON deserialize of a redacted payload returns an error (not a corrupted key)
- Unit test:
{:?}onDerivedKeydoes not contain private key bytes cargo testsucceedscargo clippysucceeds with no warnings
References
- docs/architecture/crates/vault/README.md — Known Source Drift table item #5
- docs/architecture/crates/vault/protocol.md — Serialization Redaction, Debug redaction
- docs/architecture/decisions/025-vault-local-only-dispatch.md — ADR-025 (resolves W8)
- docs/architecture/decisions/014-secret-material-flow-and-capability-injection.md — ADR-014
Notes
The redaction is defense-in-depth for logging safety, not the primary control — the primary control is that
DerivedKeynever crosses the call protocol wire (ADR-014). ADR-025 dropped the postcard/remote path that previously preserved bytes in binary formats. The custom Deserialize impl is required because#[derive(Deserialize)]would conflict and not produce the explicit redaction-rejection error. Depends on irpc removal because both modifyprotocol.rs.
Summary
Replaced DerivedKey's derived Deserialize with custom serde impls. Serialize
now always redacts private_key as "[REDACTED]" (dropped the
is_human_readable() branch that preserved bytes in binary formats). Custom
Deserialize rejects private_key == b"[REDACTED]" with an explicit error
message. Added tests for redacted-payload rejection and debug-no-leak. All tests
pass; clippy clean. Merged to develop.