Files
alknet/tasks/vault/derivedkey-serialization.md
glm-5.2 098fd8b9b9 tasks: decompose vault, core, call crates into 28 atomic implementation tasks
Break down the three initial crates (alknet-vault, alknet-core, alknet-call)
into dependency-ordered task files for implementation agents.

Structure:
- tasks/vault/ (10 tasks) — drift fixes from ADR-025/026 refactor, review,
  spec sync. Vault is independent and can run fully in parallel with core/call.
- tasks/core/ (6 tasks) — crate init, core types, config, auth, endpoint,
  review. Core is foundational; call depends on it.
- tasks/call/ (12 tasks) — split into registry/ and protocol/ topic subdirs
  reflecting the two subsystems. CallAdapter is the merge point.

Key decisions:
- Drifts 3+9+10 grouped as one task (key-versioning-rotation) — the complete
  ADR-021 rotation feature that doesn't compile in pieces
- Reviews injected at end of each crate phase (vault, core, call)
- Vault spec-sync task removes the drift table and bumps doc status to stable
- ACME deferred in core/endpoint (noted as TODO; X509 manual certs for now)
- OperationEnv kept as a trait (load-bearing for ADR-024 layering)

Validated: 28 tasks, no cycles, 11 generations of parallel work.
Critical path runs through call (11 tasks). Vault completes by generation 4.
6 high-risk tasks identified (21%): irpc-removal, endpoint, operation-context,
operation-env, call-adapter, abort-cascade.
2026-06-23 12:41:47 +00:00

5.2 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 pending
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

To be filled on completion