Files
alknet/tasks/vault/derivedkey-serialization.md

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
vault/irpc-removal
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

  • DerivedKey does not derive Serialize or Deserialize via #[derive]
  • Custom Serialize impl always redacts private_key as "[REDACTED]"
  • Custom Deserialize impl rejects private_key == b"[REDACTED]" with explicit error
  • Custom Debug impl redacts private_key as "[REDACTED]"
  • Postcard round-trip tests removed
  • Unit test: JSON serialize produces "[REDACTED]" for private_key
  • Unit test: JSON deserialize of a redacted payload returns an error (not a corrupted key)
  • Unit test: {:?} on DerivedKey does not contain private key bytes
  • cargo test succeeds
  • cargo clippy succeeds 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 DerivedKey never 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 modify protocol.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.