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

136 lines
5.9 KiB
Markdown

# 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**| |