chronogrid/docs/ARCHITECTURE/ADR/0008-net-node-identity-swar...

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.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 <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

  • 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<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