# ADR-0008: chronogrid_net — Node Identity & Swarm Key (Task 2.2) **Date:** 2026-08-22 **Status:** Accepted **Deciders:** Architecture Review Board (automated agent cycle) **Task:** 2.2 — Implement chronogrid_net: Node Identity & Swarm Key --- ## Context Task 2.2 requires implementing the foundational identity and cluster-authentication primitives for the `chronogrid_net` crate. Every ChronoGrid node must have a stable, cryptographically verifiable identity and a mechanism to restrict mesh participation to authorized devices only. The security specification (`docs/ARCHITECTURE/02-security-and-crypto.md`) mandates: - **Node Identity:** Ed25519 keypair; public key acts as the `NodeId`. - **Swarm Key:** 256-bit pre-shared secret; unauthenticated nodes cannot join. - **Wire Protocol:** Noise XK (X25519 + ChaCha20-Poly1305) — requires the Ed25519 identity for the static key role. --- ## Decision ### 1. Ed25519 Keypair via `ed25519-dalek v2` `ed25519-dalek` v2 is the current stable, audited Rust implementation of Ed25519. It integrates cleanly with `rand` v0.8 via the `rand_core` feature and produces `SigningKey` / `VerifyingKey` types that are compatible with the libp2p identity layer (Task 2.3 will wrap these into a `libp2p::identity::Keypair`). **Alternative considered:** Using `libp2p::identity` directly for key generation. **Rejected because:** Task 2.2 must not introduce the full libp2p dependency tree yet; identity primitives should be independently testable without the network stack. ### 2. NodeId = SHA-256(public key bytes) The `NodeId` is derived by SHA-256-hashing the 32-byte Ed25519 public key, producing a 32-byte (64 hex char) stable identifier. - Matches the `node_peers.id` column definition in `schema.sql` (`NodeId: Ed25519 public key hash (hex)`). - SHA-256 is collision-resistant and produces a fixed-length output suitable for use as a database primary key and display string. - The `NodeId` is distinct from the raw public key, preventing accidental exposure of the key material in log output. ### 3. SwarmKey = 32 random bytes (OsRng) The Swarm Key is generated using `OsRng` (OS-backed CSPRNG) and stored as a 64-character lowercase hex string. This matches the `node_peers.swarm_key_fingerprint` schema column which stores the first 16 hex chars of `SHA-256(swarm_key_bytes)`. **Fingerprint design:** Only the first 8 bytes (16 hex chars) of the SHA-256 hash are stored in the database, providing a non-reversible display identifier without exposing the full key. ### 4. Zeroize on Drop Both `NodeIdentity` (via `ZeroizeOnDrop` derive) and `SwarmKey` (via `ZeroizeOnDrop` derive) scrub their secret bytes from memory when dropped. `PersistedIdentity` (the JSON serialization helper) also derives `Zeroize + ZeroizeOnDrop` to prevent secret key hex strings from lingering in heap memory. ### 5. Persistence Format | Artifact | Format | Location (recommended) | |--------------|---------------------------------|-------------------------------------------| | NodeIdentity | JSON with hex-encoded key fields | `/identity.json` | | SwarmKey | 64-char hex string (plain text) | `/swarm.key` | Both files must reside inside the encrypted vault directory (SQLCipher-protected at rest). The caller (`chronogrid_daemon` / Flutter app) is responsible for placing them in the correct location. ### 6. Constant-Time Equality for SwarmKey `SwarmKey::eq` uses a byte-by-byte XOR accumulation pattern with `std::hint::black_box` to prevent the compiler from short-circuiting the comparison, mitigating timing side-channel attacks when comparing swarm keys. --- ## Consequences ### Positive - `NodeIdentity` and `SwarmKey` are independently testable without any network stack dependency. - The `NodeId` hex string maps directly to the `node_peers.id` primary key, enabling zero-transformation database inserts. - `SwarmKey::fingerprint()` maps directly to `node_peers.swarm_key_fingerprint`, enabling schema-aligned peer registry entries. - All secret material is zeroized on drop, satisfying the zero-trust local storage mandate. - 30 unit tests cover: generation uniqueness, determinism, hex round-trips, serialization round-trips, file persistence, error paths, and security properties (debug output redaction, constant-time equality). ### Negative / Trade-offs - The JSON persistence format for `NodeIdentity` stores the secret key in hex. This is acceptable only because the file resides inside the encrypted vault directory. A future hardening pass (Phase 5) may encrypt the identity file itself using the master key from `chronogrid_crypto`. - `ed25519-dalek` v2 and `libp2p` use compatible but distinct key type wrappers. Task 2.3 will need a thin conversion layer (`SigningKey` → `libp2p::identity::ed25519::Keypair`). --- ## Module Structure ``` crates/chronogrid_net/ ├── Cargo.toml # ed25519-dalek, rand, sha2, hex, serde, thiserror, zeroize └── src/ ├── lib.rs # Public re-exports: NetError, NetResult, NodeId, NodeIdentity, SwarmKey ├── error.rs # NetError enum + NetResult alias ├── identity.rs # NodeId, NodeIdentity, generate_node_identity(), load_or_create_identity() └── swarm_key.rs # SwarmKey: generate, from_bytes, from_hex, fingerprint, save, load ``` --- ## Test Coverage | Module | Tests | Key Scenarios | |---------------|-------|----------------------------------------------------------------------| | `identity` | 11 | Uniqueness, determinism, hex format, round-trip, file I/O, tampering | | `swarm_key` | 16 | Generation, hex/bytes round-trips, error paths, fingerprint, file I/O, security | | **Total** | **27**| |