5.9 KiB
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.idcolumn definition inschema.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
NodeIdis 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 | <vault_dir>/identity.json |
| SwarmKey | 64-char hex string (plain text) | <vault_dir>/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
NodeIdentityandSwarmKeyare independently testable without any network stack dependency.- The
NodeIdhex string maps directly to thenode_peers.idprimary key, enabling zero-transformation database inserts. SwarmKey::fingerprint()maps directly tonode_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
NodeIdentitystores 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 fromchronogrid_crypto. ed25519-dalekv2 andlibp2puse 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<T> 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 |